From 107f84cde8e3b3b8be70a51f79ecf8fb8201eab7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:32:38 +0100 Subject: [PATCH 001/295] implement isPolicyOwner --- src/libs/PolicyUtils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b6ee4ab3a353..80f23fd3c9ab 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -111,6 +111,11 @@ const isPolicyAdmin = (policy: OnyxEntry): boolean => policy?.role === C const isPolicyMember = (policyID: string, policies: Record): boolean => Object.values(policies).some((policy) => policy?.id === policyID); +/** + * Checks if the current user is an owner (creator) of the policy. + */ +const isPolicyOwner = (policy: OnyxEntry, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID; + /** * Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID. * @@ -244,6 +249,7 @@ export { getCleanedTagName, isPendingDeletePolicy, isPolicyMember, + isPolicyOwner, isPaidGroupPolicy, extractPolicyIDFromPath, getPathWithoutPolicyID, From 41110392095b4762c020f66bd4baebeeb57ad171 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:32:46 +0100 Subject: [PATCH 002/295] implement isReportOwner --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5f3efcbcdbb0..33ef7982af3c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4679,6 +4679,10 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry): boolean { + return report?.ownerAccountID === currentUserPersonalDetails?.accountID; +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4865,6 +4869,7 @@ export { isReportFieldOfTypeTitle, isReportFieldDisabled, getAvailableReportFields, + isReportOwner, }; export type { From 52b16ceb740c3f21c4b9c3629643db1ef4b8e52b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:45:47 +0100 Subject: [PATCH 003/295] implement draft types for leave API --- src/libs/API/parameters/LeavePolicyExpenseChatParams.ts | 6 ++++++ src/libs/API/parameters/LeaveWorkspaceParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 2 ++ src/libs/API/types.ts | 5 +++++ 4 files changed, 19 insertions(+) create mode 100644 src/libs/API/parameters/LeavePolicyExpenseChatParams.ts create mode 100644 src/libs/API/parameters/LeaveWorkspaceParams.ts diff --git a/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts b/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts new file mode 100644 index 000000000000..665b28b9e0a2 --- /dev/null +++ b/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts @@ -0,0 +1,6 @@ +type LeavePolicyExpenseChatParams = { + // TODO: Clarify + reportID: string; +}; + +export default LeavePolicyExpenseChatParams; diff --git a/src/libs/API/parameters/LeaveWorkspaceParams.ts b/src/libs/API/parameters/LeaveWorkspaceParams.ts new file mode 100644 index 000000000000..b28fa3e5aed9 --- /dev/null +++ b/src/libs/API/parameters/LeaveWorkspaceParams.ts @@ -0,0 +1,6 @@ +type LeaveWorkspaceParams = { + // TODO: Clarify + policyID: string; +}; + +export default LeaveWorkspaceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 8c0c2fde17cf..9e877fffbd2e 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -89,7 +89,9 @@ export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; +export type {default as LeavePolicyExpenseChatParams} from './LeavePolicyExpenseChatParams'; export type {default as LeaveRoomParams} from './LeaveRoomParams'; +export type {default as LeaveWorkspaceParams} from './LeaveWorkspaceParams'; export type {default as InviteToRoomParams} from './InviteToRoomParams'; export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; export type {default as FlagCommentParams} from './FlagCommentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 05b658ee0702..37ed746ea5b6 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -115,6 +115,9 @@ const WRITE_COMMANDS = { SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', SET_REPORT_NAME: 'RenameReport', + // TODO: Clarify + LEAVE_WORKSPACE: 'LeaveWorkspace', + LEAVE_POLICY_EXPENSE_CHAT: 'LeaveWorkspaceExpenseChat', } as const; type WriteCommand = ValueOf; @@ -227,6 +230,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; + [WRITE_COMMANDS.LEAVE_WORKSPACE]: Parameters.LeaveWorkspaceParams; + [WRITE_COMMANDS.LEAVE_POLICY_EXPENSE_CHAT]: Parameters.LeavePolicyExpenseChatParams; }; const READ_COMMANDS = { From 5788e867e4ebd40cde8099d19f72332206842a92 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:46:04 +0100 Subject: [PATCH 004/295] implement draft for leave API --- src/libs/actions/Policy.ts | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 0c3a8afc1576..c68bd947faf9 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; @@ -14,6 +15,8 @@ import type { DeleteMembersFromWorkspaceParams, DeleteWorkspaceAvatarParams, DeleteWorkspaceParams, + LeavePolicyExpenseChatParams, + LeaveWorkspaceParams, OpenDraftWorkspaceRequestParams, OpenWorkspaceInvitePageParams, OpenWorkspaceMembersPageParams, @@ -1984,6 +1987,48 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { return policyID; } +/** + * TODO: Comment + */ +function leaveWorkspace(policyID: string) { + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + const parameters: LeaveWorkspaceParams = { + policyID, + }; + + console.group('leaveWorkspace'); + console.log('policyID', policyID); + console.log('parameters', parameters); + console.log('{optimisticData, successData, failureData}', {optimisticData, successData, failureData}); + console.groupEnd(); + return; + API.write(WRITE_COMMANDS.LEAVE_WORKSPACE, parameters, {optimisticData, successData, failureData}); +} + +/** + * TODO: Comment + */ +function leavePolicyExpenseChat(reportID: string) { + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + const parameters: LeavePolicyExpenseChatParams = { + reportID, + }; + + console.group('leaveWorkspace'); + console.log('reportID', reportID); + console.log('parameters', parameters); + console.log('{optimisticData, successData, failureData}', {optimisticData, successData, failureData}); + console.groupEnd(); + return; + API.write(WRITE_COMMANDS.LEAVE_POLICY_EXPENSE_CHAT, parameters, {optimisticData, successData, failureData}); +} + export { removeMembers, addMembersToWorkspace, @@ -2020,4 +2065,6 @@ export { buildOptimisticPolicyRecentlyUsedTags, createDraftInitialWorkspace, setWorkspaceInviteMessageDraft, + leaveWorkspace, + leavePolicyExpenseChat, }; From 2114adb61d998fb729101975b9a7d765355f9d57 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:46:23 +0100 Subject: [PATCH 005/295] implement draft for leave a workspace chat --- src/pages/home/HeaderView.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index ca4c90b2df55..0ea2f92d3cce 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -29,10 +29,12 @@ import {getGroupChatName} from '@libs/GroupChatUtils'; import * as HeaderUtils from '@libs/HeaderUtils'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import * as Link from '@userActions/Link'; +import * as Policy from '@userActions/Policy'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; @@ -118,6 +120,8 @@ function HeaderView(props) { const isUserCreatedPolicyRoom = ReportUtils.isUserCreatedPolicyRoom(props.report); const isPolicyMember = useMemo(() => !_.isEmpty(props.policy), [props.policy]); const canLeaveRoom = ReportUtils.canLeaveRoom(props.report, isPolicyMember); + const canLeavePolicyExpenseChat = + isPolicyExpenseChat && !(PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPolicyOwner(props.policy, props.session.accountID) || ReportUtils.isReportOwner(props.report)); const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); // We hide the button when we are chatting with an automated Expensify account since it's not possible to contact @@ -156,9 +160,9 @@ function HeaderView(props) { ), ); - const canJoinOrLeave = isChatThread || isUserCreatedPolicyRoom || canLeaveRoom; + const canJoinOrLeave = isChatThread || isUserCreatedPolicyRoom || canLeaveRoom || canLeavePolicyExpenseChat; const canJoin = canJoinOrLeave && !isWhisperAction && props.report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - const canLeave = canJoinOrLeave && ((isChatThread && props.report.notificationPreference.length) || isUserCreatedPolicyRoom || canLeaveRoom); + const canLeave = canJoinOrLeave && ((isChatThread && props.report.notificationPreference.length) || isUserCreatedPolicyRoom || canLeaveRoom || canLeavePolicyExpenseChat); if (canJoin) { threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, @@ -167,10 +171,11 @@ function HeaderView(props) { }); } else if (canLeave) { const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember; + const action = isPolicyExpenseChat ? () => Policy.leavePolicyExpenseChat(props.reportID) : () => Report.leaveRoom(props.reportID, isWorkspaceMemberLeavingWorkspaceRoom); threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.leave'), - onSelected: Session.checkIfActionIsAllowed(() => Report.leaveRoom(props.reportID, isWorkspaceMemberLeavingWorkspaceRoom)), + onSelected: Session.checkIfActionIsAllowed(action), }); } @@ -360,7 +365,7 @@ export default memo( }, policy: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - selector: (policy) => _.pick(policy, ['name', 'avatar', 'pendingAction']), + selector: (policy) => _.pick(policy, ['name', 'avatar', 'pendingAction', 'role', 'ownerAccountID']), }, rootParentReportPolicy: { key: ({report}) => { From eb6299247215dff7c64c877273acb0f3949552aa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 15:46:31 +0100 Subject: [PATCH 006/295] implement draft for leave a workspace --- src/pages/workspace/WorkspacesListPage.tsx | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 9b763120b30d..c8e518429557 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -28,11 +28,12 @@ import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; import * as App from '@userActions/App'; import * as Policy from '@userActions/Policy'; +import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount, Report} from '@src/types/onyx'; +import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount, Report, Session as SessionType} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -74,6 +75,9 @@ type WorkspaceListPageOnyxProps = { /** All reports shared with the user (coming from Onyx) */ reports: OnyxCollection; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; }; type WorkspaceListPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceListPageOnyxProps; @@ -109,7 +113,7 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports}: WorkspaceListPageProps) { +function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports, session}: WorkspaceListPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -135,6 +139,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r const getMenuItem = useCallback( ({item, index}: GetMenuItem) => { const isAdmin = item.role === CONST.POLICY.ROLE.ADMIN; + const isOwner = item.ownerAccountID === session?.accountID; // Menu options to navigate to the chat report of #admins and #announce room. // For navigation, the chat report ids may be unavailable due to the missing chat reports in Onyx. // In such cases, let us use the available chat report ids from the policy. @@ -168,6 +173,15 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r }); } + if (!(isAdmin || isOwner)) { + threeDotsMenuItems.push({ + icon: Expensicons.ChatBubbles, + text: translate('common.leave'), + // TODO: Integrate a handler + onSelected: Session.checkIfActionIsAllowed(() => Policy.leaveWorkspace(item.policyID ?? '')), + }); + } + return ( ); }, - [isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate], + [session?.accountID, styles.ph5, styles.mh5, styles.mb3, styles.hoveredComponentBG, translate, isSmallScreenWidth], ); const listHeaderComponent = useCallback(() => { @@ -409,5 +423,8 @@ export default withPolicyAndFullscreenLoading( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, + session: { + key: ONYXKEYS.SESSION, + }, })(WorkspacesListPage), ); From 3775ad084b4ed838a7b4ad32793c2d9568ecc9e1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Fri, 2 Feb 2024 17:53:42 +0100 Subject: [PATCH 007/295] integrate draft api data --- .../AppNavigator/ReportScreenIDSetter.ts | 26 ++------- src/libs/ReportUtils.ts | 48 +++++++++++++++- src/libs/actions/Policy.ts | 56 +++++++++++++++---- src/libs/actions/Report.ts | 42 +------------- 4 files changed, 95 insertions(+), 77 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index b4bb56262860..927e4c57d66b 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -7,7 +7,7 @@ import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/onyx'; +import type {Policy, PolicyMembers, Report} from '@src/types/onyx'; import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { @@ -23,9 +23,6 @@ type ReportScreenIDSetterComponentProps = { /** Whether user is a new user */ isFirstTimeNewExpensifyUser: OnyxEntry; - /** The report metadata */ - reportMetadata: OnyxCollection; - /** The accountID of the current user */ accountID?: number; }; @@ -41,25 +38,15 @@ const getLastAccessedReportID = ( policies: OnyxCollection, isFirstTimeNewExpensifyUser: OnyxEntry, openOnAdminRoom: boolean, - reportMetadata: OnyxCollection, policyID?: string, policyMemberAccountIDs?: number[], ): string | undefined => { - const lastReport = ReportUtils.findLastAccessedReport( - reports, - ignoreDefaultRooms, - policies, - !!isFirstTimeNewExpensifyUser, - openOnAdminRoom, - reportMetadata, - policyID, - policyMemberAccountIDs, - ); + const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, !!isFirstTimeNewExpensifyUser, openOnAdminRoom, policyID, policyMemberAccountIDs); return lastReport?.reportID; }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); @@ -84,7 +71,6 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav policies, isFirstTimeNewExpensifyUser, !!reports?.params?.openOnAdminRoom, - reportMetadata, activeWorkspaceID, policyMemberAccountIDs, ); @@ -96,7 +82,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav } else { App.confirmReadyToOpenApp(); } - }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, policyMembers, accountID]); + }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, activeWorkspaceID, policyMembers, accountID]); // The ReportScreen without the reportID set will display a skeleton // until the reportID is loaded and set in the route param @@ -122,10 +108,6 @@ export default withOnyx session?.accountID, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 33ef7982af3c..3f48f550a077 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -491,6 +491,13 @@ Onyx.connect({ }, }); +let reportMetadata: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + waitForCollectionCallback: true, + callback: (value) => (reportMetadata = value), +}); + function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } @@ -911,7 +918,7 @@ function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMem /** * Given an array of reports, return them sorted by the last read timestamp. */ -function sortReportsByLastRead(reports: Report[], reportMetadata: OnyxCollection): Array> { +function sortReportsByLastRead(reports: Report[]): Array> { return reports .filter((report) => !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime)) .sort((a, b) => { @@ -991,7 +998,6 @@ function findLastAccessedReport( policies: OnyxCollection, isFirstTimeNewExpensifyUser: boolean, openOnAdminRoom = false, - reportMetadata: OnyxCollection = {}, policyID?: string, policyMemberAccountIDs: number[] = [], ): OnyxEntry { @@ -1007,7 +1013,7 @@ function findLastAccessedReport( reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID); } - let sortedReports = sortReportsByLastRead(reportsValues, reportMetadata); + let sortedReports = sortReportsByLastRead(reportsValues); let adminReport: OnyxEntry | undefined; if (openOnAdminRoom) { @@ -4683,6 +4689,41 @@ function isReportOwner(report: OnyxEntry): boolean { return report?.ownerAccountID === currentUserPersonalDetails?.accountID; } +function navigateUserOnceLeaveReport(reportID: string) { + const sortedReportsByLastRead = sortReportsByLastRead(Object.values(allReports ?? {}) as Report[]); + + // We want to filter out the current report, hidden reports and empty chats + const filteredReportsByLastRead = sortedReportsByLastRead.filter( + (sortedReport) => + sortedReport?.reportID !== reportID && + sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + shouldReportBeInOptionList({ + report: sortedReport, + currentReportId: '', + isInGSDMode: false, + betas: [], + policies: {}, + excludeEmptyChats: true, + doesReportHaveViolations: false, + }), + ); + const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; + + if (lastAccessedReportID) { + // We should call Navigation.goBack to pop the current route first before navigating to Concierge. + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID)); + } else { + const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); + const chat = getChatByParticipants(participantAccountIDs); + if (chat?.reportID) { + // We should call Navigation.goBack to pop the current route first before navigating to Concierge. + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); + } + } +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4870,6 +4911,7 @@ export { isReportFieldDisabled, getAvailableReportFields, isReportOwner, + navigateUserOnceLeaveReport, }; export type { diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index c68bd947faf9..929318ed9856 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1991,30 +1991,60 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { * TODO: Comment */ function leaveWorkspace(policyID: string) { - const optimisticData: OnyxUpdate[] = []; - const successData: OnyxUpdate[] = []; - const failureData: OnyxUpdate[] = []; - + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + avatar: '', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + errors: null, + }, + }, + ]; const parameters: LeaveWorkspaceParams = { policyID, }; console.group('leaveWorkspace'); console.log('policyID', policyID); - console.log('parameters', parameters); - console.log('{optimisticData, successData, failureData}', {optimisticData, successData, failureData}); + console.log('{parameters, optimisticData}', {parameters, optimisticData}); console.groupEnd(); - return; - API.write(WRITE_COMMANDS.LEAVE_WORKSPACE, parameters, {optimisticData, successData, failureData}); + + API.write(WRITE_COMMANDS.LEAVE_WORKSPACE, parameters, {optimisticData}); } /** * TODO: Comment */ function leavePolicyExpenseChat(reportID: string) { - const optimisticData: OnyxUpdate[] = []; - const successData: OnyxUpdate[] = []; - const failureData: OnyxUpdate[] = []; + const report = ReportUtils.getReport(reportID); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: report, + }, + ]; const parameters: LeavePolicyExpenseChatParams = { reportID, @@ -2025,8 +2055,10 @@ function leavePolicyExpenseChat(reportID: string) { console.log('parameters', parameters); console.log('{optimisticData, successData, failureData}', {optimisticData, successData, failureData}); console.groupEnd(); - return; + API.write(WRITE_COMMANDS.LEAVE_POLICY_EXPENSE_CHAT, parameters, {optimisticData, successData, failureData}); + + ReportUtils.navigateUserOnceLeaveReport(reportID); } export { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 782cf2b174c2..27cc7a2af4a7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -66,7 +66,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportUserIsTyping} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; @@ -159,13 +159,6 @@ Onyx.connect({ }, }); -let reportMetadata: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_METADATA, - waitForCollectionCallback: true, - callback: (value) => (reportMetadata = value), -}); - const allReports: OnyxCollection = {}; let conciergeChatReportID: string | undefined; const typingWatchTimers: Record = {}; @@ -2248,38 +2241,7 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal API.write(WRITE_COMMANDS.LEAVE_ROOM, parameters, {optimisticData, successData, failureData}); - const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata); - - // We want to filter out the current report, hidden reports and empty chats - const filteredReportsByLastRead = sortedReportsByLastRead.filter( - (sortedReport) => - sortedReport?.reportID !== reportID && - sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && - ReportUtils.shouldReportBeInOptionList({ - report: sortedReport, - currentReportId: '', - isInGSDMode: false, - betas: [], - policies: {}, - excludeEmptyChats: true, - doesReportHaveViolations: false, - }), - ); - const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; - - if (lastAccessedReportID) { - // We should call Navigation.goBack to pop the current route first before navigating to Concierge. - Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID)); - } else { - const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); - const chat = ReportUtils.getChatByParticipants(participantAccountIDs); - if (chat?.reportID) { - // We should call Navigation.goBack to pop the current route first before navigating to Concierge. - Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); - } - } + ReportUtils.navigateUserOnceLeaveReport(reportID); } /** Invites people to a room */ From 95b8741eb659e4e9b83776fdc40545a523585953 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 5 Feb 2024 17:48:58 +0100 Subject: [PATCH 008/295] use real api for leaving workspaces --- .../LeavePolicyExpenseChatParams.ts | 6 - .../API/parameters/LeaveWorkspaceParams.ts | 6 - src/libs/API/parameters/index.ts | 2 - src/libs/API/types.ts | 5 - src/libs/actions/Policy.ts | 126 ++++++------------ src/pages/home/HeaderView.js | 4 +- src/pages/workspace/WorkspacesListPage.tsx | 3 +- 7 files changed, 46 insertions(+), 106 deletions(-) delete mode 100644 src/libs/API/parameters/LeavePolicyExpenseChatParams.ts delete mode 100644 src/libs/API/parameters/LeaveWorkspaceParams.ts diff --git a/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts b/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts deleted file mode 100644 index 665b28b9e0a2..000000000000 --- a/src/libs/API/parameters/LeavePolicyExpenseChatParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -type LeavePolicyExpenseChatParams = { - // TODO: Clarify - reportID: string; -}; - -export default LeavePolicyExpenseChatParams; diff --git a/src/libs/API/parameters/LeaveWorkspaceParams.ts b/src/libs/API/parameters/LeaveWorkspaceParams.ts deleted file mode 100644 index b28fa3e5aed9..000000000000 --- a/src/libs/API/parameters/LeaveWorkspaceParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -type LeaveWorkspaceParams = { - // TODO: Clarify - policyID: string; -}; - -export default LeaveWorkspaceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a41227a60507..b7c3dff7c342 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -90,9 +90,7 @@ export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; export type {default as AddEmojiReactionParams} from './AddEmojiReactionParams'; export type {default as RemoveEmojiReactionParams} from './RemoveEmojiReactionParams'; -export type {default as LeavePolicyExpenseChatParams} from './LeavePolicyExpenseChatParams'; export type {default as LeaveRoomParams} from './LeaveRoomParams'; -export type {default as LeaveWorkspaceParams} from './LeaveWorkspaceParams'; export type {default as InviteToRoomParams} from './InviteToRoomParams'; export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; export type {default as FlagCommentParams} from './FlagCommentParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index aa1f601ed575..c011fa395f0f 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -116,9 +116,6 @@ const WRITE_COMMANDS = { SET_NAME_VALUE_PAIR: 'SetNameValuePair', SET_REPORT_FIELD: 'Report_SetFields', SET_REPORT_NAME: 'RenameReport', - // TODO: Clarify - LEAVE_WORKSPACE: 'LeaveWorkspace', - LEAVE_POLICY_EXPENSE_CHAT: 'LeaveWorkspaceExpenseChat', } as const; type WriteCommand = ValueOf; @@ -232,8 +229,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_NAME_VALUE_PAIR]: Parameters.SetNameValuePairParams; [WRITE_COMMANDS.SET_REPORT_FIELD]: Parameters.SetReportFieldParams; [WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams; - [WRITE_COMMANDS.LEAVE_WORKSPACE]: Parameters.LeaveWorkspaceParams; - [WRITE_COMMANDS.LEAVE_POLICY_EXPENSE_CHAT]: Parameters.LeavePolicyExpenseChatParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 929318ed9856..d718fcd1b88a 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -15,8 +15,6 @@ import type { DeleteMembersFromWorkspaceParams, DeleteWorkspaceAvatarParams, DeleteWorkspaceParams, - LeavePolicyExpenseChatParams, - LeaveWorkspaceParams, OpenDraftWorkspaceRequestParams, OpenWorkspaceInvitePageParams, OpenWorkspaceMembersPageParams, @@ -41,6 +39,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, PolicyMember, PolicyTags, RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {CustomUnit} from '@src/types/onyx/Policy'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AnnounceRoomMembersOnyxData = { @@ -354,8 +353,8 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] /** * Build optimistic data for removing users from the announcement room */ -function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: number[]): AnnounceRoomMembersOnyxData { - const announceReport = ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID); +function removeOptimisticAnnounceRoomMembers(policy: Policy | EmptyObject, accountIDs: number[]): AnnounceRoomMembersOnyxData { + const announceReport = ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policy.id); const announceRoomMembers: AnnounceRoomMembersOnyxData = { onyxOptimisticData: [], onyxFailureData: [], @@ -367,21 +366,37 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: numbe if (announceReport?.participantAccountIDs) { const remainUsers = announceReport.participantAccountIDs.filter((e) => !accountIDs.includes(e)); + announceRoomMembers.onyxOptimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, value: { participantAccountIDs: [...remainUsers], visibleChatMemberAccountIDs: [...remainUsers], + ...(accountIDs.includes(sessionAccountID) + ? { + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + oldPolicyName: policy.name, + hasDraft: false, + } + : {}), }, }); - announceRoomMembers.onyxFailureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, value: { participantAccountIDs: announceReport.participantAccountIDs, visibleChatMemberAccountIDs: announceReport.visibleChatMemberAccountIDs, + ...(accountIDs.includes(sessionAccountID) + ? { + statusNum: announceReport.statusNum, + stateNum: announceReport.stateNum, + oldPolicyName: announceReport.oldPolicyName, + hasDraft: announceReport.hasDraft, + } + : {}), }, }); } @@ -404,7 +419,7 @@ function removeMembers(accountIDs: number[], policyID: string) { const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs); const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy.name, CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY)); - const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policyID, accountIDs); + const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy, accountIDs); const optimisticMembersState: OnyxCollection = {}; const successMembersState: OnyxCollection = {}; @@ -507,11 +522,34 @@ function removeMembers(accountIDs: number[], policyID: string) { }); }); + if (accountIDs.includes(sessionAccountID)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingAction: policy.pendingAction, + }, + }); + } + const params: DeleteMembersFromWorkspaceParams = { emailList: accountIDs.map((accountID) => allPersonalDetails?.[accountID]?.login).join(','), policyID, }; + console.log('WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}', WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, { + optimisticData, + successData, + failureData, + }); + API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } @@ -1987,80 +2025,6 @@ function createWorkspaceFromIOUPayment(iouReport: Report): string | undefined { return policyID; } -/** - * TODO: Comment - */ -function leaveWorkspace(policyID: string) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - avatar: '', - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - errors: null, - }, - }, - ]; - const parameters: LeaveWorkspaceParams = { - policyID, - }; - - console.group('leaveWorkspace'); - console.log('policyID', policyID); - console.log('{parameters, optimisticData}', {parameters, optimisticData}); - console.groupEnd(); - - API.write(WRITE_COMMANDS.LEAVE_WORKSPACE, parameters, {optimisticData}); -} - -/** - * TODO: Comment - */ -function leavePolicyExpenseChat(reportID: string) { - const report = ReportUtils.getReport(reportID); - - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - }, - }, - ]; - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - }, - }, - ]; - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: report, - }, - ]; - - const parameters: LeavePolicyExpenseChatParams = { - reportID, - }; - - console.group('leaveWorkspace'); - console.log('reportID', reportID); - console.log('parameters', parameters); - console.log('{optimisticData, successData, failureData}', {optimisticData, successData, failureData}); - console.groupEnd(); - - API.write(WRITE_COMMANDS.LEAVE_POLICY_EXPENSE_CHAT, parameters, {optimisticData, successData, failureData}); - - ReportUtils.navigateUserOnceLeaveReport(reportID); -} - export { removeMembers, addMembersToWorkspace, @@ -2097,6 +2061,4 @@ export { buildOptimisticPolicyRecentlyUsedTags, createDraftInitialWorkspace, setWorkspaceInviteMessageDraft, - leaveWorkspace, - leavePolicyExpenseChat, }; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index e957c70ed5cc..9cd78b47271b 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -34,7 +34,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import reportPropTypes from '@pages/reportPropTypes'; import * as Link from '@userActions/Link'; -import * as Policy from '@userActions/Policy'; import * as Report from '@userActions/Report'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; @@ -164,11 +163,10 @@ function HeaderView(props) { }); } else if (canLeave) { const isWorkspaceMemberLeavingWorkspaceRoom = !isChatThread && lodashGet(props.report, 'visibility', '') === CONST.REPORT.VISIBILITY.RESTRICTED && isPolicyMember; - const action = isPolicyExpenseChat ? () => Policy.leavePolicyExpenseChat(props.reportID) : () => Report.leaveRoom(props.reportID, isWorkspaceMemberLeavingWorkspaceRoom); threeDotMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.leave'), - onSelected: Session.checkIfActionIsAllowed(action), + onSelected: Session.checkIfActionIsAllowed(() => Report.leaveRoom(props.reportID, isWorkspaceMemberLeavingWorkspaceRoom)), }); } diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index c8e518429557..bda81eee0011 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -177,8 +177,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r threeDotsMenuItems.push({ icon: Expensicons.ChatBubbles, text: translate('common.leave'), - // TODO: Integrate a handler - onSelected: Session.checkIfActionIsAllowed(() => Policy.leaveWorkspace(item.policyID ?? '')), + onSelected: Session.checkIfActionIsAllowed(() => Policy.removeMembers([session?.accountID ?? 0], item.policyID ?? '')), }); } From af8c733a9e824b07f3accc4131994fc4a0f579fe Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 5 Feb 2024 17:51:55 +0100 Subject: [PATCH 009/295] Revert "integrate draft api data" This reverts commit 3775ad084b4ed838a7b4ad32793c2d9568ecc9e1. --- .../AppNavigator/ReportScreenIDSetter.ts | 26 ++++++++-- src/libs/ReportUtils.ts | 48 ++----------------- src/libs/actions/Report.ts | 42 +++++++++++++++- 3 files changed, 65 insertions(+), 51 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index 927e4c57d66b..b4bb56262860 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -7,7 +7,7 @@ import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyMembers, Report} from '@src/types/onyx'; +import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/onyx'; import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { @@ -23,6 +23,9 @@ type ReportScreenIDSetterComponentProps = { /** Whether user is a new user */ isFirstTimeNewExpensifyUser: OnyxEntry; + /** The report metadata */ + reportMetadata: OnyxCollection; + /** The accountID of the current user */ accountID?: number; }; @@ -38,15 +41,25 @@ const getLastAccessedReportID = ( policies: OnyxCollection, isFirstTimeNewExpensifyUser: OnyxEntry, openOnAdminRoom: boolean, + reportMetadata: OnyxCollection, policyID?: string, policyMemberAccountIDs?: number[], ): string | undefined => { - const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, !!isFirstTimeNewExpensifyUser, openOnAdminRoom, policyID, policyMemberAccountIDs); + const lastReport = ReportUtils.findLastAccessedReport( + reports, + ignoreDefaultRooms, + policies, + !!isFirstTimeNewExpensifyUser, + openOnAdminRoom, + reportMetadata, + policyID, + policyMemberAccountIDs, + ); return lastReport?.reportID; }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); @@ -71,6 +84,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav policies, isFirstTimeNewExpensifyUser, !!reports?.params?.openOnAdminRoom, + reportMetadata, activeWorkspaceID, policyMemberAccountIDs, ); @@ -82,7 +96,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav } else { App.confirmReadyToOpenApp(); } - }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, activeWorkspaceID, policyMembers, accountID]); + }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, reportMetadata, activeWorkspaceID, policyMembers, accountID]); // The ReportScreen without the reportID set will display a skeleton // until the reportID is loaded and set in the route param @@ -108,6 +122,10 @@ export default withOnyx session?.accountID, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 045ed1d6a1e2..7594d9db0fdd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -503,13 +503,6 @@ Onyx.connect({ }, }); -let reportMetadata: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_METADATA, - waitForCollectionCallback: true, - callback: (value) => (reportMetadata = value), -}); - function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } @@ -930,7 +923,7 @@ function filterReportsByPolicyIDAndMemberAccountIDs(reports: Report[], policyMem /** * Given an array of reports, return them sorted by the last read timestamp. */ -function sortReportsByLastRead(reports: Report[]): Array> { +function sortReportsByLastRead(reports: Report[], reportMetadata: OnyxCollection): Array> { return reports .filter((report) => !!report?.reportID && !!(reportMetadata?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${report.reportID}`]?.lastVisitTime ?? report?.lastReadTime)) .sort((a, b) => { @@ -1010,6 +1003,7 @@ function findLastAccessedReport( policies: OnyxCollection, isFirstTimeNewExpensifyUser: boolean, openOnAdminRoom = false, + reportMetadata: OnyxCollection = {}, policyID?: string, policyMemberAccountIDs: number[] = [], ): OnyxEntry { @@ -1025,7 +1019,7 @@ function findLastAccessedReport( reportsValues = filterReportsByPolicyIDAndMemberAccountIDs(reportsValues, policyMemberAccountIDs, policyID); } - let sortedReports = sortReportsByLastRead(reportsValues); + let sortedReports = sortReportsByLastRead(reportsValues, reportMetadata); let adminReport: OnyxEntry | undefined; if (openOnAdminRoom) { @@ -4771,41 +4765,6 @@ function isReportOwner(report: OnyxEntry): boolean { return report?.ownerAccountID === currentUserPersonalDetails?.accountID; } -function navigateUserOnceLeaveReport(reportID: string) { - const sortedReportsByLastRead = sortReportsByLastRead(Object.values(allReports ?? {}) as Report[]); - - // We want to filter out the current report, hidden reports and empty chats - const filteredReportsByLastRead = sortedReportsByLastRead.filter( - (sortedReport) => - sortedReport?.reportID !== reportID && - sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && - shouldReportBeInOptionList({ - report: sortedReport, - currentReportId: '', - isInGSDMode: false, - betas: [], - policies: {}, - excludeEmptyChats: true, - doesReportHaveViolations: false, - }), - ); - const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; - - if (lastAccessedReportID) { - // We should call Navigation.goBack to pop the current route first before navigating to Concierge. - Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID)); - } else { - const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); - const chat = getChatByParticipants(participantAccountIDs); - if (chat?.reportID) { - // We should call Navigation.goBack to pop the current route first before navigating to Concierge. - Navigation.goBack(ROUTES.HOME); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); - } - } -} - export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4994,7 +4953,6 @@ export { isReportFieldDisabled, getAvailableReportFields, isReportOwner, - navigateUserOnceLeaveReport, getAllAncestorReportActionIDs, }; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c07b330b5a68..4bff826ceb3a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -66,7 +66,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportUserIsTyping} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; @@ -159,6 +159,13 @@ Onyx.connect({ }, }); +let reportMetadata: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_METADATA, + waitForCollectionCallback: true, + callback: (value) => (reportMetadata = value), +}); + const allReports: OnyxCollection = {}; let conciergeChatReportID: string | undefined; const typingWatchTimers: Record = {}; @@ -2241,7 +2248,38 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal API.write(WRITE_COMMANDS.LEAVE_ROOM, parameters, {optimisticData, successData, failureData}); - ReportUtils.navigateUserOnceLeaveReport(reportID); + const sortedReportsByLastRead = ReportUtils.sortReportsByLastRead(Object.values(allReports ?? {}) as Report[], reportMetadata); + + // We want to filter out the current report, hidden reports and empty chats + const filteredReportsByLastRead = sortedReportsByLastRead.filter( + (sortedReport) => + sortedReport?.reportID !== reportID && + sortedReport?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + ReportUtils.shouldReportBeInOptionList({ + report: sortedReport, + currentReportId: '', + isInGSDMode: false, + betas: [], + policies: {}, + excludeEmptyChats: true, + doesReportHaveViolations: false, + }), + ); + const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; + + if (lastAccessedReportID) { + // We should call Navigation.goBack to pop the current route first before navigating to Concierge. + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID)); + } else { + const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); + const chat = ReportUtils.getChatByParticipants(participantAccountIDs); + if (chat?.reportID) { + // We should call Navigation.goBack to pop the current route first before navigating to Concierge. + Navigation.goBack(ROUTES.HOME); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chat.reportID)); + } + } } /** Invites people to a room */ From 8a3473f35c8ed37cef7ab45dbeed45467d32c04e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 5 Feb 2024 17:54:11 +0100 Subject: [PATCH 010/295] minor fix --- src/libs/actions/Policy.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index d718fcd1b88a..d012fba90ef0 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; @@ -39,7 +38,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, PolicyMember, PolicyTags, RecentlyUsedCategories, RecentlyUsedTags, ReimbursementAccount, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {CustomUnit} from '@src/types/onyx/Policy'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AnnounceRoomMembersOnyxData = { @@ -353,8 +351,8 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] /** * Build optimistic data for removing users from the announcement room */ -function removeOptimisticAnnounceRoomMembers(policy: Policy | EmptyObject, accountIDs: number[]): AnnounceRoomMembersOnyxData { - const announceReport = ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policy.id); +function removeOptimisticAnnounceRoomMembers(policyID: string, policyName: string, accountIDs: number[]): AnnounceRoomMembersOnyxData { + const announceReport = ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID); const announceRoomMembers: AnnounceRoomMembersOnyxData = { onyxOptimisticData: [], onyxFailureData: [], @@ -377,7 +375,7 @@ function removeOptimisticAnnounceRoomMembers(policy: Policy | EmptyObject, accou ? { statusNum: CONST.REPORT.STATUS_NUM.CLOSED, stateNum: CONST.REPORT.STATE_NUM.APPROVED, - oldPolicyName: policy.name, + oldPolicyName: policyName, hasDraft: false, } : {}), @@ -419,7 +417,7 @@ function removeMembers(accountIDs: number[], policyID: string) { const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs); const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy.name, CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY)); - const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy, accountIDs); + const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy.id, policy.name, accountIDs); const optimisticMembersState: OnyxCollection = {}; const successMembersState: OnyxCollection = {}; @@ -544,12 +542,6 @@ function removeMembers(accountIDs: number[], policyID: string) { policyID, }; - console.log('WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}', WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, { - optimisticData, - successData, - failureData, - }); - API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } From 0ce1db13f56356cba7434de0603a1e0601c7b156 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 6 Feb 2024 15:28:32 +0100 Subject: [PATCH 011/295] move button to top --- src/pages/workspace/WorkspacesListPage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index bda81eee0011..b51e14adbe9e 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -157,6 +157,14 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r }); } + if (!(isAdmin || isOwner)) { + threeDotsMenuItems.push({ + icon: Expensicons.ChatBubbles, + text: translate('common.leave'), + onSelected: Session.checkIfActionIsAllowed(() => Policy.removeMembers([session?.accountID ?? 0], item.policyID ?? '')), + }); + } + if (isAdmin && item.adminRoom) { threeDotsMenuItems.push({ icon: Expensicons.Hashtag, @@ -173,14 +181,6 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r }); } - if (!(isAdmin || isOwner)) { - threeDotsMenuItems.push({ - icon: Expensicons.ChatBubbles, - text: translate('common.leave'), - onSelected: Session.checkIfActionIsAllowed(() => Policy.removeMembers([session?.accountID ?? 0], item.policyID ?? '')), - }); - } - return ( Date: Thu, 22 Feb 2024 11:04:54 +0700 Subject: [PATCH 012/295] add can edit billable --- .../ReportActionItem/MoneyRequestView.tsx | 2 + src/components/Switch.tsx | 6 ++- src/libs/ReportUtils.ts | 42 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d3c86698f910..b34a4c5a7365 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -125,6 +125,7 @@ function MoneyRequestView({ const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); + const canEditBillable = ReportUtils.canEditBillable(parentReportAction); // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat @@ -427,6 +428,7 @@ function MoneyRequestView({ accessibilityLabel={translate('common.billable')} isOn={!!transactionBillable} onToggle={saveBillable} + disabled={!canEditBillable} /> )} diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index fd9d9ae315ff..9a5ee1d7a6ea 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -14,6 +14,8 @@ type SwitchProps = { /** Accessibility label for the switch */ accessibilityLabel: string; + + disabled?: boolean; }; const OFFSET_X = { @@ -21,7 +23,7 @@ const OFFSET_X = { ON: 20, }; -function Switch({isOn, onToggle, accessibilityLabel}: SwitchProps) { +function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) { const styles = useThemeStyles(); const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF)); @@ -35,7 +37,7 @@ function Switch({isOn, onToggle, accessibilityLabel}: SwitchProps) { return ( onToggle(!isOn)} onLongPress={() => onToggle(!isOn)} role={CONST.ROLE.SWITCH} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 05e2db66d629..45056c22a44d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2147,6 +2147,47 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { return !isReportApproved(moneyRequestReport) && !isSettled(moneyRequestReport?.reportID) && isRequestor; } +function canEditBillable(reportAction: OnyxEntry): boolean { + const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); + + if (isDeleted) { + return false; + } + + // If the report action is not IOU type, return true early + if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + return true; + } + + if (reportAction.originalMessage.type !== CONST.IOU.REPORT_ACTION_TYPE.CREATE) { + return false; + } + + const moneyRequestReportID = reportAction?.originalMessage?.IOUReportID ?? 0; + + if (!moneyRequestReportID) { + return false; + } + + const moneyRequestReport = getReport(String(moneyRequestReportID)); + const isRequestor = currentUserAccountID === reportAction?.actorAccountID; + + if (isIOUReport(moneyRequestReport)) { + return isProcessingReport(moneyRequestReport) && isRequestor; + } + + const policy = getPolicy(moneyRequestReport?.policyID ?? ''); + const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; + const isManager = currentUserAccountID === moneyRequestReport?.managerID; + + // Admin & managers can always edit coding fields such as tag, category, billable, etc. As long as the report has a state higher than OPEN. + if ((isAdmin || isManager) && !isDraftExpenseReport(moneyRequestReport)) { + return true; + } + + return isRequestor; +} + /** * Checks if the current user can edit the provided property of a money request * @@ -5217,6 +5258,7 @@ export { getAllAncestorReportActionIDs, canEditPolicyDescription, getPolicyDescriptionText, + canEditBillable, }; export type { From 7e15f959f533d797ede647823507e0d676a31054 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 22 Feb 2024 11:27:53 +0700 Subject: [PATCH 013/295] handle interactive for switch --- .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/components/Switch.tsx | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index b34a4c5a7365..5d9973a0db59 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -428,7 +428,7 @@ function MoneyRequestView({ accessibilityLabel={translate('common.billable')} isOn={!!transactionBillable} onToggle={saveBillable} - disabled={!canEditBillable} + interactive={canEditBillable} /> )} diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 9a5ee1d7a6ea..4fbca927c95f 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -15,7 +15,8 @@ type SwitchProps = { /** Accessibility label for the switch */ accessibilityLabel: string; - disabled?: boolean; + /** Whether the menu item should be interactive at all */ + interactive?: boolean; }; const OFFSET_X = { @@ -23,7 +24,7 @@ const OFFSET_X = { ON: 20, }; -function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) { +function Switch({isOn, onToggle, accessibilityLabel, interactive = true}: SwitchProps) { const styles = useThemeStyles(); const offsetX = useRef(new Animated.Value(isOn ? OFFSET_X.ON : OFFSET_X.OFF)); @@ -35,11 +36,19 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) { }).start(); }, [isOn]); + const onPressOrLongPressAction = () => { + if (!interactive) { + return; + } + + onToggle(!isOn); + } + return ( onToggle(!isOn)} - onLongPress={() => onToggle(!isOn)} + style={[styles.switchTrack, !isOn && styles.switchInactive && styles.cursorDefault, !interactive && styles.cursorDefault]} + onPress={onPressOrLongPressAction} + onLongPress={onPressOrLongPressAction} role={CONST.ROLE.SWITCH} aria-checked={isOn} accessibilityLabel={accessibilityLabel} From 05764aafc0359e05ff1ae913032bcd06cae3ce33 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 22 Feb 2024 11:46:54 +0700 Subject: [PATCH 014/295] run prettier --- src/components/Switch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 4fbca927c95f..ceec4016a0c5 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -42,7 +42,7 @@ function Switch({isOn, onToggle, accessibilityLabel, interactive = true}: Switch } onToggle(!isOn); - } + }; return ( Date: Tue, 12 Mar 2024 15:30:49 -1000 Subject: [PATCH 015/295] Add Onyx key to allow remotely resetting client data --- src/ONYXKEYS.ts | 4 ++ src/libs/actions/App.ts | 37 +++++++++++++++++++ .../settings/AboutPage/TroubleshootPage.tsx | 18 +-------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8c48cbad561f..26af65ea1029 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -271,6 +271,9 @@ const ONYXKEYS = { /** Indicates whether an forced upgrade is required */ UPDATE_REQUIRED: 'updateRequired', + /** Indicates whether an forced reset is required a.k.a. clear Oynx data without signing the user out */ + RESET_REQUIRED: 'resetRequired', + /** Stores the logs of the app for debugging purposes */ LOGS: 'logs', @@ -583,6 +586,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; [ONYXKEYS.UPDATE_REQUIRED]: boolean; + [ONYXKEYS.RESET_REQUIRED]: boolean; [ONYXKEYS.PLAID_CURRENT_EVENT]: string; [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 302e4048d0e8..6d4c79fc8843 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -31,6 +31,7 @@ import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import type {OnyxData} from '@src/types/onyx/Request'; +import type {OnyxKey} from '@src/ONYXKEYS'; import * as Policy from './Policy'; import * as Session from './Session'; import Timing from './Timing'; @@ -77,6 +78,41 @@ Onyx.connect({ }, }); +const KEYS_TO_PRESERVE: OnyxKey[] = [ + ONYXKEYS.ACCOUNT, + ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, + ONYXKEYS.IS_LOADING_APP, + ONYXKEYS.IS_SIDEBAR_LOADED, + ONYXKEYS.MODAL, + ONYXKEYS.NETWORK, + ONYXKEYS.SESSION, + ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, + ONYXKEYS.NVP_TRY_FOCUS_MODE, + ONYXKEYS.PREFERRED_THEME, + ONYXKEYS.NVP_PREFERRED_LOCALE, + ONYXKEYS.CREDENTIALS, +]; + +let previousResetRequired: boolean|null = false; +Onyx.connect({ + key: ONYXKEYS.RESET_REQUIRED, + callback: (isResetRequired) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (previousResetRequired || !isResetRequired) { + return; + } + + previousResetRequired = isResetRequired; + Onyx.clear(KEYS_TO_PRESERVE).then(() => { + // Set this to false to reset the flag for this client + Onyx.set(ONYXKEYS.RESET_REQUIRED, false); + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + openApp(); + }); + }, +}); + let resolveIsReadyPromise: () => void; const isReadyToOpenApp = new Promise((resolve) => { resolveIsReadyPromise = resolve; @@ -529,4 +565,5 @@ export { savePolicyDraftByNewWorkspace, createWorkspaceWithPolicyDraftAndNavigateToIt, updateLastVisitedPath, + KEYS_TO_PRESERVE, }; diff --git a/src/pages/settings/AboutPage/TroubleshootPage.tsx b/src/pages/settings/AboutPage/TroubleshootPage.tsx index 9bc756df03cb..5f7dfbc33323 100644 --- a/src/pages/settings/AboutPage/TroubleshootPage.tsx +++ b/src/pages/settings/AboutPage/TroubleshootPage.tsx @@ -24,25 +24,9 @@ import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxKey} from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -const keysToPreserve: OnyxKey[] = [ - ONYXKEYS.ACCOUNT, - ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, - ONYXKEYS.IS_LOADING_APP, - ONYXKEYS.IS_SIDEBAR_LOADED, - ONYXKEYS.MODAL, - ONYXKEYS.NETWORK, - ONYXKEYS.SESSION, - ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, - ONYXKEYS.NVP_TRY_FOCUS_MODE, - ONYXKEYS.PREFERRED_THEME, - ONYXKEYS.NVP_PREFERRED_LOCALE, - ONYXKEYS.CREDENTIALS, -]; - type BaseMenuItem = { translationKey: TranslationPaths; icon: React.FC; @@ -136,7 +120,7 @@ function TroubleshootPage({shouldStoreLogs}: TroubleshootPageProps) { isVisible={isConfirmationModalVisible} onConfirm={() => { setIsConfirmationModalVisible(false); - Onyx.clear(keysToPreserve).then(() => { + Onyx.clear(App.KEYS_TO_PRESERVE).then(() => { App.openApp(); }); }} From ea0362330104183037148e50cdfc9ab9a7ab51c4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 26 Mar 2024 12:51:25 +0100 Subject: [PATCH 016/295] integrate leave policy command --- src/libs/API/parameters/LeavePolicyParams.ts | 7 +++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Policy.ts | 15 +++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 src/libs/API/parameters/LeavePolicyParams.ts diff --git a/src/libs/API/parameters/LeavePolicyParams.ts b/src/libs/API/parameters/LeavePolicyParams.ts new file mode 100644 index 000000000000..bc2962eebb66 --- /dev/null +++ b/src/libs/API/parameters/LeavePolicyParams.ts @@ -0,0 +1,7 @@ +type LeavePolicyParams = { + authToken: string; + policyID: string; + email: string; +}; + +export default LeavePolicyParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 1895c2426e1a..a3a7fc3cd836 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -196,3 +196,4 @@ export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTax export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams'; export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams'; export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams'; +export type {default as LeavePolicyParams} from './LeavePolicyParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 9d6e6b3929b8..f4945821a14a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -193,6 +193,7 @@ const WRITE_COMMANDS = { UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue', SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled', DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates', + LEAVE_POLICY: 'LeavePolicy', } as const; type WriteCommand = ValueOf; @@ -384,6 +385,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; + [WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 4b3dea92ba2e..fd4108bf0e64 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -24,6 +24,7 @@ import type { EnablePolicyTagsParams, EnablePolicyTaxesParams, EnablePolicyWorkflowsParams, + LeavePolicyParams, OpenDraftWorkspaceRequestParams, OpenPolicyCategoriesPageParams, OpenPolicyDistanceRatesPageParams, @@ -194,11 +195,13 @@ Onyx.connect({ let sessionEmail = ''; let sessionAccountID = 0; +let sessionAuthToken = ''; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { sessionEmail = val?.email ?? ''; sessionAccountID = val?.accountID ?? -1; + sessionAuthToken = val?.authToken ?? ''; }, }); @@ -961,6 +964,17 @@ function removeMembers(accountIDs: number[], policyID: string) { pendingAction: policy.pendingAction, }, }); + + const params: LeavePolicyParams = { + policyID, + email: sessionEmail, + authToken: sessionAuthToken, + }; + + // TODO: Extract into a distinct function + API.write(WRITE_COMMANDS.LEAVE_POLICY, params, {optimisticData, successData, failureData}); + + return; } const params: DeleteMembersFromWorkspaceParams = { @@ -968,6 +982,7 @@ function removeMembers(accountIDs: number[], policyID: string) { policyID, }; + // eslint-disable-next-line rulesdir/no-multiple-api-calls API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData}); } From 5ca68a6e77f9fabb67431c7347593414247d722f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 27 Mar 2024 11:39:37 +0100 Subject: [PATCH 017/295] Add leave policy change log const --- src/CONST.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.ts b/src/CONST.ts index 47caa5e64a90..660bb994b047 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -729,6 +729,7 @@ const CONST = { UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME', UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED', UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE', + LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY', }, ROOMCHANGELOG: { INVITE_TO_ROOM: 'INVITETOROOM', From 532dcacf98ff02b8999695dfc469a06fb4151926 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:01:20 +0100 Subject: [PATCH 018/295] refactor all base pages to tsx --- ...e.js => IOURequestRedirectToStartPage.tsx} | 23 +++------- ...stStartPage.js => IOURequestStartPage.tsx} | 0 ...StepAmount.js => IOURequestStepAmount.tsx} | 0 ...Distance.js => IOURequestStepDistance.tsx} | 45 +++++++------------ 4 files changed, 21 insertions(+), 47 deletions(-) rename src/pages/iou/request/{IOURequestRedirectToStartPage.js => IOURequestRedirectToStartPage.tsx} (73%) rename src/pages/iou/request/{IOURequestStartPage.js => IOURequestStartPage.tsx} (100%) rename src/pages/iou/request/step/{IOURequestStepAmount.js => IOURequestStepAmount.tsx} (100%) rename src/pages/iou/request/step/{IOURequestStepDistance.js => IOURequestStepDistance.tsx} (92%) diff --git a/src/pages/iou/request/IOURequestRedirectToStartPage.js b/src/pages/iou/request/IOURequestRedirectToStartPage.tsx similarity index 73% rename from src/pages/iou/request/IOURequestRedirectToStartPage.js rename to src/pages/iou/request/IOURequestRedirectToStartPage.tsx index 2da235743705..c34fa6065028 100644 --- a/src/pages/iou/request/IOURequestRedirectToStartPage.js +++ b/src/pages/iou/request/IOURequestRedirectToStartPage.tsx @@ -1,34 +1,22 @@ import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; -import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type { WithWritableReportOrNotFoundProps } from './step/withWritableReportOrNotFound'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired, - - /** The type of IOU Request, i.e. manual, scan, distance */ - iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, - }), - }).isRequired, -}; +type IOURequestRedirectToStartPageProps = WithWritableReportOrNotFoundProps; function IOURequestRedirectToStartPage({ route: { params: {iouType, iouRequestType}, }, -}) { - const isIouTypeValid = _.values(CONST.IOU.TYPE).includes(iouType); - const isIouRequestTypeValid = _.values(CONST.IOU.REQUEST_TYPE).includes(iouRequestType); +}: IOURequestRedirectToStartPageProps) { + const isIouTypeValid = Object.values(CONST.IOU.TYPE).includes(iouType); + const isIouRequestTypeValid = Object.values(CONST.IOU.REQUEST_TYPE).includes(iouRequestType); useEffect(() => { if (!isIouTypeValid || !isIouRequestTypeValid) { @@ -64,6 +52,5 @@ function IOURequestRedirectToStartPage({ } IOURequestRedirectToStartPage.displayName = 'IOURequestRedirectToStartPage'; -IOURequestRedirectToStartPage.propTypes = propTypes; export default IOURequestRedirectToStartPage; diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.tsx similarity index 100% rename from src/pages/iou/request/IOURequestStartPage.js rename to src/pages/iou/request/IOURequestStartPage.tsx diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.tsx similarity index 100% rename from src/pages/iou/request/step/IOURequestStepAmount.js rename to src/pages/iou/request/step/IOURequestStepAmount.tsx diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.tsx similarity index 92% rename from src/pages/iou/request/step/IOURequestStepDistance.js rename to src/pages/iou/request/step/IOURequestStepDistance.tsx index dad610cbc636..d725cdfe152f 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -1,7 +1,8 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import { withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Button from '@components/Button'; import DistanceRequestFooter from '@components/DistanceRequest/DistanceRequestFooter'; @@ -17,7 +18,6 @@ import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as TransactionUtils from '@libs/TransactionUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import * as MapboxToken from '@userActions/MapboxToken'; @@ -25,50 +25,39 @@ import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import type * as OnyxTypes from '@src/types/onyx'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type { WithWritableReportOrNotFoundProps } from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** The report that the transaction belongs to */ - report: reportPropTypes, - - /** The transaction object being modified in Onyx */ - transaction: transactionPropTypes, - - /** backup version of the original transaction */ - transactionBackup: transactionPropTypes, -}; +type IOURequestStepDistanceOnyxProps = { + transactionBackup?: OnyxEntry, +} -const defaultProps = { - report: {}, - transaction: {}, - transactionBackup: {}, +type IOURequestStepDistanceProps = WithWritableReportOrNotFoundProps & IOURequestStepDistanceOnyxProps & { + report?: OnyxEntry, + transaction?: OnyxEntry, }; function IOURequestStepDistance({ - report, route: { params: {action, iouType, reportID, transactionID, backTo}, }, + report, transaction, transactionBackup, -}) { +}: IOURequestStepDistanceProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const [optimisticWaypoints, setOptimisticWaypoints] = useState(null); const waypoints = useMemo(() => optimisticWaypoints || lodashGet(transaction, 'comment.waypoints', {waypoint0: {}, waypoint1: {}}), [optimisticWaypoints, transaction]); - const waypointsList = _.keys(waypoints); + const waypointsList = Object.keys(waypoints); const previousWaypoints = usePrevious(waypoints); - const numberOfWaypoints = _.size(waypoints); - const numberOfPreviousWaypoints = _.size(previousWaypoints); + const numberOfWaypoints = Object.keys(waypoints).length; + const numberOfPreviousWaypoints = Object.keys(previousWaypoints).length; const scrollViewRef = useRef(null); const isLoadingRoute = lodashGet(transaction, 'comment.isLoading', false); const isLoading = lodashGet(transaction, 'isLoading', false); @@ -82,7 +71,7 @@ function IOURequestStepDistance({ const [shouldShowAtLeastTwoDifferentWaypointsError, setShouldShowAtLeastTwoDifferentWaypointsError] = useState(false); const nonEmptyWaypointsCount = useMemo(() => _.filter(_.keys(waypoints), (key) => !_.isEmpty(waypoints[key])).length, [waypoints]); const duplicateWaypointsError = useMemo(() => nonEmptyWaypointsCount >= 2 && _.size(validatedWaypoints) !== nonEmptyWaypointsCount, [nonEmptyWaypointsCount, validatedWaypoints]); - const atLeastTwoDifferentWaypointsError = useMemo(() => _.size(validatedWaypoints) < 2, [validatedWaypoints]); + const atLeastTwoDifferentWaypointsError = useMemo(() => Object.keys(validatedWaypoints) < 2, [validatedWaypoints]); const isEditing = action === CONST.IOU.ACTION.EDIT; const isCreatingNewRequest = Navigation.getActiveRoute().includes('start'); @@ -284,8 +273,6 @@ function IOURequestStepDistance({ } IOURequestStepDistance.displayName = 'IOURequestStepDistance'; -IOURequestStepDistance.propTypes = propTypes; -IOURequestStepDistance.defaultProps = defaultProps; export default compose( withWritableReportOrNotFound, From 9f68f82a9901ddad5012226fa009f3128adc6226 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 27 Mar 2024 15:09:11 +0100 Subject: [PATCH 019/295] add correct props for IOURequestStartPage --- src/pages/iou/request/IOURequestStartPage.tsx | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index cb078fac133c..3852e04dfe04 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -1,9 +1,9 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import { withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -11,7 +11,6 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; -import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; @@ -23,53 +22,34 @@ import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import reportPropTypes from '@pages/reportPropTypes'; +import type * as OnyxTypes from '@src/types/onyx'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import IOURequestStepAmount from './step/IOURequestStepAmount'; import IOURequestStepDistance from './step/IOURequestStepDistance'; -import IOURequestStepRoutePropTypes from './step/IOURequestStepRoutePropTypes'; import IOURequestStepScan from './step/IOURequestStepScan'; +import type { WithWritableReportOrNotFoundProps } from './step/withWritableReportOrNotFound'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** The report that holds the transaction */ - report: reportPropTypes, - - /** The policy tied to the report */ - policy: PropTypes.shape({ - /** Type of the policy */ - type: PropTypes.string, - }), - - /** The tab to select by default (whatever the user visited last) */ - selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)), - - /** The transaction being modified */ - transaction: transactionPropTypes, +type IOURequestStartPageOnyxProps = { + report?: OnyxEntry, + policy?: OnyxEntry, + selectedTab?: typeof CONST.TAB_REQUEST[keyof typeof CONST.TAB_REQUEST], + transaction?: OnyxEntry, }; -const defaultProps = { - report: {}, - policy: {}, - selectedTab: CONST.TAB_REQUEST.SCAN, - transaction: {}, -}; +type IOURequestStartPageProps = WithWritableReportOrNotFoundProps & IOURequestStartPageOnyxProps; function IOURequestStartPage({ - report, - policy, route, route: { params: {iouType, reportID}, }, + report, + policy, selectedTab, transaction, -}) { +}: IOURequestStartPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const navigation = useNavigation(); From 4b11894ead580896b430914be894c2a0df08a27f Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 28 Mar 2024 16:12:16 +0100 Subject: [PATCH 020/295] Sync keyboard navigation in SelectionList and PopoverMenu --- src/components/MenuItem.tsx | 5 + src/components/PopoverMenu.tsx | 14 +- src/components/PopoverMenuItem.tsx | 29 ++ src/components/SelectionList/BaseListItem.tsx | 25 +- .../SelectionList/BaseSelectionList.tsx | 248 +++++++++--------- .../SelectionList/RadioListItem.tsx | 2 + .../SelectionList/TableListItem.tsx | 2 + src/components/SelectionList/types.ts | 3 + 8 files changed, 198 insertions(+), 130 deletions(-) create mode 100644 src/components/PopoverMenuItem.tsx diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 110256ba166b..730d4d025d95 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -246,6 +246,9 @@ type MenuItemBaseProps = { /** Adds padding to the left of the text when there is no icon. */ shouldPutLeftPaddingWhenNoIcon?: boolean; + + /** Handles what to do when the item is focused */ + onFocus?: () => void; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -317,6 +320,7 @@ function MenuItem( contentFit = 'cover', isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, + onFocus, }: MenuItemProps, ref: ForwardedRef, ) { @@ -447,6 +451,7 @@ function MenuItem( role={CONST.ROLE.MENUITEM} accessibilityLabel={title ? title.toString() : ''} accessible + onFocus={onFocus} > {({pressed}) => ( <> diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 1fd1c8ef5a3b..c2c8685aa528 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -12,10 +12,11 @@ import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import * as Expensicons from './Icon/Expensicons'; import type {MenuItemProps} from './MenuItem'; import MenuItem from './MenuItem'; +import PopoverMenuItem from './PopoverMenuItem'; import PopoverWithMeasuredContent from './PopoverWithMeasuredContent'; import Text from './Text'; -type PopoverMenuItem = MenuItemProps & { +type PopoverMenuListItem = MenuItemProps & { /** Text label */ text: string; @@ -23,7 +24,7 @@ type PopoverMenuItem = MenuItemProps & { onSelected?: () => void; /** Sub menu items to be rendered after a menu item is selected */ - subMenuItems?: PopoverMenuItem[]; + subMenuItems?: PopoverMenuListItem[]; /** Determines whether the menu item is disabled or not */ disabled?: boolean; @@ -39,10 +40,10 @@ type PopoverMenuProps = Partial & { isVisible: boolean; /** Callback to fire when a CreateMenu item is selected */ - onItemSelected: (selectedItem: PopoverMenuItem, index: number) => void; + onItemSelected: (selectedItem: PopoverMenuListItem, index: number) => void; /** Menu items to be rendered on the list */ - menuItems: PopoverMenuItem[]; + menuItems: PopoverMenuListItem[]; /** Optional non-interactive text to display as a header for any create menu */ headerText?: string; @@ -193,7 +194,7 @@ function PopoverMenu({ {!!headerText && {headerText}} {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} {currentMenuItems.map((item, menuIndex) => ( - setFocusedIndex(menuIndex)} /> ))} @@ -225,4 +227,4 @@ function PopoverMenu({ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo(PopoverMenu); -export type {PopoverMenuItem, PopoverMenuProps}; +export type {PopoverMenuListItem, PopoverMenuProps}; diff --git a/src/components/PopoverMenuItem.tsx b/src/components/PopoverMenuItem.tsx new file mode 100644 index 000000000000..ff4e12a2f9b1 --- /dev/null +++ b/src/components/PopoverMenuItem.tsx @@ -0,0 +1,29 @@ +import React, {useLayoutEffect, useRef} from 'react'; +import type {View} from 'react-native'; +import MenuItem from './MenuItem'; +import type {MenuItemProps} from './MenuItem'; + +function PopoverMenuItem(props: MenuItemProps) { + const ref = useRef(null); + + // Sync focus on an item + useLayoutEffect(() => { + if (!props.focused) { + return; + } + + ref?.current?.focus(); + }, [props.focused]); + + return ( + + ); +} + +PopoverMenuItem.displayName = 'PopoverMenuItem'; + +export default PopoverMenuItem; diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 42fdc7dc575e..458f05aaef8c 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useLayoutEffect, useRef} from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -31,12 +31,16 @@ function BaseListItem({ pendingAction, FooterComponent, children, + isFocused, + onFocus = () => {}, }: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {hovered, bind} = useHover(); + const pressableRef = useRef(null); + const rightHandSideComponentRender = () => { if (canSelectMultiple || !rightHandSideComponent) { return null; @@ -57,6 +61,15 @@ function BaseListItem({ } }; + // Sync focus on an item + useLayoutEffect(() => { + if (!isFocused) { + return; + } + + pressableRef?.current?.focus(); + }, [isFocused]); + return ( onDismissError(item)} @@ -68,6 +81,7 @@ function BaseListItem({ onSelectRow(item)} disabled={isDisabled} accessibilityLabel={item.text ?? ''} @@ -78,6 +92,7 @@ function BaseListItem({ onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} nativeID={keyForList ?? ''} style={pressableStyle} + onFocus={onFocus} > {canSelectMultiple && checkmarkPosition === CONST.DIRECTION.LEFT && ( @@ -132,6 +147,14 @@ function BaseListItem({ )} + {!item.isSelected && item.brickRoadIndicator && [CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR].includes(item.brickRoadIndicator) && ( + + + + )} {rightHandSideComponentRender()} {FooterComponent} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 32cd89854cff..b8cd19fe4171 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -4,7 +4,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; -import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; import FixedFooter from '@components/FixedFooter'; @@ -16,6 +15,7 @@ import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useActiveElementRole from '@hooks/useActiveElementRole'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; @@ -161,9 +161,6 @@ function BaseSelectionList( }; }, [canSelectMultiple, sections]); - // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member - const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey)); - const [slicedSections, ShowMoreButtonInstance] = useMemo(() => { let remainingOptionsLimit = CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * currentPage; const processedSections = sections.map((section) => { @@ -218,6 +215,17 @@ function BaseSelectionList( [flattenedSections.allOptions], ); + // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey), + maxIndex: flattenedSections.allOptions.length - 1, + isActive: true, + onFocusedIndexChange: (index: number) => { + setFocusedIndex(index); + scrollToIndex(index, true); + }, + }); + /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * @@ -335,6 +343,7 @@ function BaseSelectionList( checkmarkPosition={checkmarkPosition} keyForList={item.keyForList ?? ''} isMultilineSupported={isRowMultilineSupported} + onFocus={() => setFocusedIndex(index)} /> ); }; @@ -473,128 +482,121 @@ function BaseSelectionList( ); return ( - section.data).length - 1} - onFocusedIndexChanged={updateAndScrollToFocusedIndex} - > - - {({safeAreaPaddingBottomStyle}) => ( - - {shouldShowTextInput && ( - - { - innerTextInputRef.current = element as RNTextInput; - - if (!textInputRef) { - return; - } - - // eslint-disable-next-line no-param-reassign - textInputRef.current = element as RNTextInput; - }} - label={textInputLabel} - accessibilityLabel={textInputLabel} - hint={textInputHint} - role={CONST.ROLE.PRESENTATION} - value={textInputValue} - placeholder={textInputPlaceholder} - maxLength={textInputMaxLength} - onChangeText={onChangeText} - inputMode={inputMode} - selectTextOnFocus - spellCheck={false} - onSubmitEditing={selectFocusedOption} - blurOnSubmit={!!flattenedSections.allOptions.length} - isLoading={isLoadingNewOptions} - testID="selection-list-text-input" - /> - - )} - {!!headerMessage && ( - - {headerMessage} - - )} - {!!headerContent && headerContent} - {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( - - ) : ( - <> - {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - - - + {({safeAreaPaddingBottomStyle}) => ( + + {shouldShowTextInput && ( + + { + innerTextInputRef.current = element as RNTextInput; + + if (!textInputRef) { + return; + } + + // eslint-disable-next-line no-param-reassign + textInputRef.current = element as RNTextInput; + }} + label={textInputLabel} + accessibilityLabel={textInputLabel} + hint={textInputHint} + role={CONST.ROLE.PRESENTATION} + value={textInputValue} + placeholder={textInputPlaceholder} + maxLength={textInputMaxLength} + onChangeText={onChangeText} + inputMode={inputMode} + selectTextOnFocus + spellCheck={false} + onSubmitEditing={selectFocusedOption} + blurOnSubmit={!!flattenedSections.allOptions.length} + isLoading={isLoadingNewOptions} + testID="selection-list-text-input" + /> + + )} + {!!headerMessage && ( + + {headerMessage} + + )} + {!!headerContent && headerContent} + {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( + + ) : ( + <> + {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( + + + + {!customListHeader && ( + - {!customListHeader && ( - e.preventDefault() : undefined} - > - {translate('workspace.people.selectAll')} - - )} - - {customListHeader} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + > + {translate('workspace.people.selectAll')} + + )} - )} - {!headerMessage && !canSelectMultiple && customListHeader} - item.keyForList ?? `${index}`} - extraData={focusedIndex} - indicatorStyle="white" - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} - maxToRenderPerBatch={maxToRenderPerBatch} - windowSize={5} - viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} - testID="selection-list" - onLayout={onSectionListLayout} - style={(!maxToRenderPerBatch || isInitialSectionListRender) && styles.opacity0} - ListFooterComponent={ShowMoreButtonInstance} - /> - {children} - - )} - {showConfirmButton && ( - -