From 9f57e200b87294a39867472146753acea9b77f2f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:10:10 +0530 Subject: [PATCH 001/421] Add tests for group chat name --- __mocks__/@react-native-reanimated/index.ts | 10 + src/pages/InviteReportParticipantsPage.tsx | 17 +- tests/ui/GroupChatNameTests.tsx | 393 ++++++++++++++++++++ tests/ui/UnreadIndicatorsTest.tsx | 8 +- tests/unit/ReportUtilsTest.ts | 94 +++++ tests/utils/LHNTestUtils.tsx | 20 +- tests/utils/TestHelper.ts | 59 +++ 7 files changed, 584 insertions(+), 17 deletions(-) create mode 100644 __mocks__/@react-native-reanimated/index.ts create mode 100644 tests/ui/GroupChatNameTests.tsx diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts new file mode 100644 index 000000000000..28efba1dde69 --- /dev/null +++ b/__mocks__/@react-native-reanimated/index.ts @@ -0,0 +1,10 @@ +// __mocks__/react-native-reanimated/index.js +const actualAnimated = jest.requireActual('react-native-reanimated/mock'); + +const mock = { + ...actualAnimated, + createAnimatedPropAdapter: jest.fn(), + useReducedMotion: jest.fn(), +}; + +export default mock; diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index a4d5c5518ba2..4db57f5f2f01 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -11,6 +11,7 @@ import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem import type {Section} from '@components/SelectionList/types'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -44,7 +45,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [recentReports, setRecentReports] = useState([]); @@ -57,7 +58,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, false, options.reports, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], debouncedSearchTerm, excludedUsers, false, options.reports, true); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -77,7 +78,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen setRecentReports(inviteOptions.recentReports); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, searchTerm, excludedUsers, options]); + }, [personalDetails, betas, debouncedSearchTerm, excludedUsers, options]); const sections = useMemo(() => { const sectionsArr: Sections = []; @@ -88,11 +89,11 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; - if (searchTerm !== '') { + if (debouncedSearchTerm !== '') { filterSelectedOptions = selectedOptions.filter((option) => { const accountID = option?.accountID; const isOptionInPersonalDetails = invitePersonalDetails.some((personalDetail) => accountID && personalDetail?.accountID === accountID); - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm); const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); @@ -130,7 +131,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen } return sectionsArr; - }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, areOptionsInitialized, recentReports]); + }, [invitePersonalDetails, debouncedSearchTerm, selectedOptions, translate, userToInvite, areOptionsInitialized, recentReports]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -171,7 +172,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen }, [selectedOptions, backRoute, reportID, validate]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); const expensifyEmails = CONST.EXPENSIFY_EMAILS as string[]; if (!userToInvite && expensifyEmails.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); @@ -187,7 +188,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen return translate('messages.userIsAlreadyMember', {login: searchValue, name: reportName ?? ''}); } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, !!userToInvite, searchValue); - }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + }, [debouncedSearchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); const footerContent = useMemo( () => ( diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx new file mode 100644 index 000000000000..e0ac71120768 --- /dev/null +++ b/tests/ui/GroupChatNameTests.tsx @@ -0,0 +1,393 @@ +/* eslint-disable testing-library/no-node-access */ +import type * as NativeNavigation from '@react-navigation/native'; +import {act, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import * as Localize from '@libs/Localize'; +import * as AppActions from '@userActions/App'; +import * as User from '@userActions/User'; +import App from '@src/App'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Participant} from '@src/types/onyx/Report'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import {navigateToSidebarOption} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +// We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App +jest.setTimeout(50000); + +jest.mock('../../src/components/ConfirmedRoute.tsx'); + +// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest +jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + __esModule: true, + default: { + ignoreLogs: jest.fn(), + ignoreAllLogs: jest.fn(), + }, +})); + +/** + * We need to keep track of the transitionEnd callback so we can trigger it in our tests + */ +let transitionEndCB: () => void; + +type ListenerMock = { + triggerTransitionEnd: () => void; + addListener: jest.Mock; +}; + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * This can't be moved to a utils file because Jest wants any external function to stay in the scope. + * Details: https://github.com/jestjs/jest/issues/2567 + */ +const createAddListenerMock = (): ListenerMock => { + const transitionEndListeners: Array<() => void> = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback: () => void) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + const {triggerTransitionEnd, addListener} = createAddListenerMock(); + transitionEndCB = triggerTransitionEnd; + + const useNavigation = () => + ({ + navigate: jest.fn(), + ...actualNav.useNavigation, + getState: () => ({ + routes: [], + }), + addListener, + } as typeof NativeNavigation.useNavigation); + + return { + ...actualNav, + useNavigation, + getState: () => ({ + routes: [], + }), + } as typeof NativeNavigation; +}); + +beforeAll(() => { + TestHelper.beforeAllSetupUITests(); +}); + +const REPORT_ID = '1'; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; +const USER_C_ACCOUNT_ID = 3; +const USER_C_EMAIL = 'user_c@test.com'; +const USER_D_ACCOUNT_ID = 4; +const USER_D_EMAIL = 'user_d@test.com'; +const USER_E_ACCOUNT_ID = 5; +const USER_E_EMAIL = 'user_e@test.com'; +const USER_F_ACCOUNT_ID = 6; +const USER_F_EMAIL = 'user_f@test.com'; +const USER_G_ACCOUNT_ID = 7; +const USER_G_EMAIL = 'user_g@test.com'; +const USER_H_ACCOUNT_ID = 8; +const USER_H_EMAIL = 'user_h@test.com'; + +/** + * Sets up a test with a logged in user. Returns the test instance. + */ +function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Promise { + // Render the App and sign in as a test user. + render(); + + const participants: Record = {}; + participantAccountIDs?.forEach((id) => { + participants[id] = { + hidden: false, + role: id === 1 ? CONST.REPORT.ROLE.ADMIN : CONST.REPORT.ROLE.MEMBER, + } as Participant; + }); + + return waitForBatchedUpdatesWithAct() + .then(async () => { + await waitForBatchedUpdatesWithAct(); + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + return waitForBatchedUpdatesWithAct(); + }) + .then(() => { + User.subscribeToUserEvents(); + return waitForBatchedUpdates(); + }) + .then(async () => { + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName, + lastMessageText: 'Test', + participants, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + }); + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_A_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_A_EMAIL, USER_A_ACCOUNT_ID, 'A'), + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + [USER_D_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_D_EMAIL, USER_D_ACCOUNT_ID, 'D'), + [USER_E_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_E_EMAIL, USER_E_ACCOUNT_ID, 'E'), + [USER_F_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_F_EMAIL, USER_F_ACCOUNT_ID, 'F'), + [USER_G_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_G_EMAIL, USER_G_ACCOUNT_ID, 'G'), + [USER_H_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_H_EMAIL, USER_H_ACCOUNT_ID, 'H'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + return waitForBatchedUpdatesWithAct(); + }); +} + +/** + * Tests for checking the group chat names at places like LHN, chat header, details page etc. + * Note that limit of 5 names is only for the header. + */ +describe('Tests for group chat name', () => { + beforeEach(() => { + jest.clearAllMocks(); + Onyx.clear(); + + // Unsubscribe to pusher channels + PusherHelper.teardown(); + }); + + const participantAccountIDs4 = [USER_A_ACCOUNT_ID, USER_B_ACCOUNT_ID, USER_C_ACCOUNT_ID, USER_D_ACCOUNT_ID]; + const participantAccountIDs8 = [...participantAccountIDs4, USER_E_ACCOUNT_ID, USER_F_ACCOUNT_ID, USER_G_ACCOUNT_ID, USER_H_ACCOUNT_ID]; + + it('Should show correctly in LHN', () => + signInAndGetApp('A, B, C, D', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D')); + })); + + it('Should show correctly in LHN when report name is not present', () => + signInAndGetApp('', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D')); + })); + + it('Should show all 8 names in LHN when 8 participants are present', () => + signInAndGetApp('', participantAccountIDs8).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H')); + })); + + it('Check if group name shows fine for report header', () => + signInAndGetApp('', participantAccountIDs4) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'A, B, C, D'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show only 5 names when there are 8 participants in the report header', () => + signInAndGetApp('', participantAccountIDs8) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'A, B, C, D, E'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show exact name in header when report name is available with 4 participants', () => + signInAndGetApp('Test chat', participantAccountIDs4) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('Test chat'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'Test chat'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show exact name in header when report name is available with 8 participants', () => + signInAndGetApp("Let's talk", participantAccountIDs8) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe("Let's talk"); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = "Let's talk"; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show last message preview in LHN', () => + signInAndGetApp('A, B, C, D', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const lastChatHintText = Localize.translateLocal('accessibilityHints.lastChatMessagePreview'); + const lastChatText = screen.queryByLabelText(lastChatHintText); + + return waitFor(() => expect(lastChatText?.props?.children).toBe('B: Test')); + })); + + it('Should sort the names before displaying', () => + signInAndGetApp('', [USER_E_ACCOUNT_ID, ...participantAccountIDs4]).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E')); + })); +}); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 1d31a707d81d..1a0bd3aeb279 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -25,6 +25,7 @@ import type {ReportAction, ReportActions} from '@src/types/onyx'; import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; +import {navigateToSidebarOption} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -82,13 +83,6 @@ function navigateToSidebar(): Promise { return waitForBatchedUpdates(); } -async function navigateToSidebarOption(index: number): Promise { - const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); - const optionRows = screen.queryAllByAccessibilityHint(hintText); - fireEvent(optionRows[index], 'press'); - await waitForBatchedUpdatesWithAct(); -} - function areYouOnChatListScreen(): boolean { const hintText = Localize.translateLocal('sidebarScreen.listOfChats'); const sidebarLinks = screen.queryAllByLabelText(hintText); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 598c0e3bcbd6..29502ba6177e 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -10,6 +10,7 @@ import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; +import {fakePersonalDetails} from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Be sure to include the mocked permissions library or else the beta tests won't work @@ -991,4 +992,97 @@ describe('ReportUtils', () => { expect(report).toEqual(undefined); }); }); + + describe('getGroupChatName tests', () => { + afterEach(() => Onyx.clear()); + + describe('When participantAccountIDs is passed to getGroupChatName', () => { + it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4])).toEqual('Four, One, Three, Two'); + }); + + it('Should show all participants name if count <= 5 and shouldApplyLimit is true', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('Four, One, Three, Two'); + }); + + it('Should show 5 participants name if count > 5 and shouldApplyLimit is true', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], true)).toEqual('Five, Four, One, Three, Two'); + }); + + it('Should show all participants name if count > 5 and shouldApplyLimit is false', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + }); + + it('Should use correct display name for participants', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, participantsPersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); + }); + }); + + describe('When participantAccountIDs is not passed to getGroupChatName and report ID is passed', () => { + it('Should show report name if count <= 5 and shouldApplyLimit is false', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, false, [1]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count <= 5 and shouldApplyLimit is true', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, false, [1]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, true, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count > 5 and shouldApplyLimit is true', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, true, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count > 5 and shouldApplyLimit is false', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual("Let's talk"); + }); + + it('Should show participant names if report name is not available', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: '', + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + }); + }); + }); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 7197529cd43c..1d8a13ead7d6 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -112,6 +112,13 @@ const fakePersonalDetails: PersonalDetailsList = { avatar: 'none', firstName: 'Nine', }, + 10: { + accountID: 10, + login: 'email10@test.com', + displayName: 'Email Ten', + avatar: 'none', + firstName: 'Ten', + }, }; let lastFakeReportID = 0; @@ -120,16 +127,25 @@ let lastFakeReportActionID = 0; /** * @param millisecondsInThePast the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) */ -function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0, isUnread = false): Report { +function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0, isUnread = false, adminIDs: number[] = []): Report { const lastVisibleActionCreated = DateUtils.getDBTime(Date.now() - millisecondsInThePast); + const participants = ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs); + + adminIDs.forEach((id) => { + participants[id] = { + hidden: false, + role: CONST.REPORT.ROLE.ADMIN, + }; + }); + return { type: CONST.REPORT.TYPE.CHAT, reportID: `${++lastFakeReportID}`, reportName: 'Report', lastVisibleActionCreated, lastReadTime: isUnread ? DateUtils.subtractMillisecondsFromDateTime(lastVisibleActionCreated, 1) : lastVisibleActionCreated, - participants: ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs), + participants, }; } diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 84ea2b2aafbe..6f97c23a5b29 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,3 +1,4 @@ +import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; @@ -13,6 +14,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from './waitForBatchedUpdatesWithAct'; +import * as Localize from '@libs/Localize'; type MockFetch = jest.MockedFn & { pause: () => void; @@ -305,6 +308,59 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners: Listener[] = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +async function navigateToSidebarOption(index: number): Promise { + const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(hintText); + fireEvent(optionRows[index], 'press'); + await waitForBatchedUpdatesWithAct(); +} + +function beforeAllSetupUITests(shouldConnectToPusher = false) { + // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it + // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means + // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling + // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to + // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. + global.fetch = getGlobalFetchMock(); + + Linking.setInitialURL('https://new.expensify.com/'); + appSetup(); + + if (shouldConnectToPusher) { + PusherConnectionManager.init(); + Pusher.init({ + appKey: CONFIG.PUSHER.APP_KEY, + cluster: CONFIG.PUSHER.CLUSTER, + authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, + }); + } +} + export type {MockFetch, FormData}; export { assertFormDataMatchesObject, @@ -318,4 +374,7 @@ export { expectAPICommandToHaveBeenCalled, expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, + createAddListenerMock, + navigateToSidebarOption, + beforeAllSetupUITests, }; From 7b5298a9e61ceeefdb87d3398902a6038602bffa Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 2 Aug 2024 02:34:04 +0530 Subject: [PATCH 002/421] Update --- __mocks__/@react-native-reanimated/index.ts | 3 ++- tests/ui/GroupChatNameTests.tsx | 4 ++++ tests/utils/TestHelper.ts | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts index 28efba1dde69..df9cc4ecef8d 100644 --- a/__mocks__/@react-native-reanimated/index.ts +++ b/__mocks__/@react-native-reanimated/index.ts @@ -1,4 +1,5 @@ -// __mocks__/react-native-reanimated/index.js +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + const actualAnimated = jest.requireActual('react-native-reanimated/mock'); const mock = { diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index e0ac71120768..7eb70b412f70 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -1,4 +1,8 @@ /* eslint-disable testing-library/no-node-access */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type * as NativeNavigation from '@react-navigation/native'; import {act, render, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 6f97c23a5b29..b10f165a96be 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,8 +1,10 @@ import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; +import type {Listener} from 'onfido-sdk-ui/types/shared/EventEmitter'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; +import * as Localize from '@libs/Localize'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; @@ -15,7 +17,6 @@ import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from './waitForBatchedUpdatesWithAct'; -import * as Localize from '@libs/Localize'; type MockFetch = jest.MockedFn & { pause: () => void; @@ -318,6 +319,7 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { const createAddListenerMock = () => { const transitionEndListeners: Listener[] = []; const triggerTransitionEnd = () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); }; From a428070ae660713f0a7bfab57631b20a4c9b00fe Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:38:39 +0530 Subject: [PATCH 003/421] Update --- tests/ui/GroupChatNameTests.tsx | 50 +-------------------------------- tests/utils/TestHelper.ts | 1 - 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 7eb70b412f70..fa84ee1e12d5 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -45,55 +45,7 @@ type ListenerMock = { addListener: jest.Mock; }; -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * Details: https://github.com/jestjs/jest/issues/2567 - */ -const createAddListenerMock = (): ListenerMock => { - const transitionEndListeners: Array<() => void> = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback: () => void) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - const {triggerTransitionEnd, addListener} = createAddListenerMock(); - transitionEndCB = triggerTransitionEnd; - - const useNavigation = () => - ({ - navigate: jest.fn(), - ...actualNav.useNavigation, - getState: () => ({ - routes: [], - }), - addListener, - } as typeof NativeNavigation.useNavigation); - - return { - ...actualNav, - useNavigation, - getState: () => ({ - routes: [], - }), - } as typeof NativeNavigation; -}); +jest.mock('@react-navigation/native'); beforeAll(() => { TestHelper.beforeAllSetupUITests(); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index b10f165a96be..f80ce747837c 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,6 +1,5 @@ import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; -import type {Listener} from 'onfido-sdk-ui/types/shared/EventEmitter'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; From e8956bcc2b2c5e77054675f39dc8a740a27f3742 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:44:44 +0530 Subject: [PATCH 004/421] Update TestHelper.ts --- tests/utils/TestHelper.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index f80ce747837c..72dbfaafe61f 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -308,32 +308,6 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners: Listener[] = []; - const triggerTransitionEnd = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - async function navigateToSidebarOption(index: number): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); const optionRows = screen.queryAllByAccessibilityHint(hintText); @@ -375,7 +349,6 @@ export { expectAPICommandToHaveBeenCalled, expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, - createAddListenerMock, navigateToSidebarOption, beforeAllSetupUITests, }; From 3e2f30e6eacb00fe7d154a859d49574a3f726b09 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:11:44 +0530 Subject: [PATCH 005/421] Update --- tests/unit/ReportUtilsTest.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 334244ec066e..9543e02c3c73 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -996,30 +996,48 @@ describe('ReportUtils', () => { describe('getGroupChatName tests', () => { afterEach(() => Onyx.clear()); + const fourParticipants = [ + {accountID: 1, login: "email1@test.com"}, + {accountID: 2, login: "email2@test.com"}, + {accountID: 3, login: "email3@test.com"}, + {accountID: 4, login: "email4@test.com"}, + ] + + const eightParticipants = [ + {accountID: 1, login: "email1@test.com"}, + {accountID: 2, login: "email2@test.com"}, + {accountID: 3, login: "email3@test.com"}, + {accountID: 4, login: "email4@test.com"}, + {accountID: 5, login: "email5@test.com"}, + {accountID: 6, login: "email6@test.com"}, + {accountID: 7, login: "email7@test.com"}, + {accountID: 8, login: "email8@test.com"}, + ] + describe('When participantAccountIDs is passed to getGroupChatName', () => { it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4])).toEqual('Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(fourParticipants)).toEqual('Four, One, Three, Two'); }); it('Should show all participants name if count <= 5 and shouldApplyLimit is true', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(fourParticipants)).toEqual('Four, One, Three, Two'); }); it('Should show 5 participants name if count > 5 and shouldApplyLimit is true', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], true)).toEqual('Five, Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(eightParticipants, true)).toEqual('Five, Four, One, Three, Two'); }); it('Should show all participants name if count > 5 and shouldApplyLimit is false', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + expect(ReportUtils.getGroupChatName(eightParticipants, false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); }); it('Should use correct display name for participants', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, participantsPersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); + expect(ReportUtils.getGroupChatName(fourParticipants, true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); }); }); From 84deb67dd9c5e3003286a160f994e0b7ae40e2c6 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:16:01 +0530 Subject: [PATCH 006/421] Update --- tests/ui/GroupChatNameTests.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index fa84ee1e12d5..25defa2ff283 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -3,7 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import type * as NativeNavigation from '@react-navigation/native'; import {act, render, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; @@ -40,11 +39,6 @@ jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ */ let transitionEndCB: () => void; -type ListenerMock = { - triggerTransitionEnd: () => void; - addListener: jest.Mock; -}; - jest.mock('@react-navigation/native'); beforeAll(() => { From 221b3ff1399fb6120b2fa8d7fb8016626b3bf8c3 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:31:49 +0530 Subject: [PATCH 007/421] Lint fixes --- tests/unit/ReportUtilsTest.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 9543e02c3c73..bf8f48e988ab 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -997,22 +997,22 @@ describe('ReportUtils', () => { afterEach(() => Onyx.clear()); const fourParticipants = [ - {accountID: 1, login: "email1@test.com"}, - {accountID: 2, login: "email2@test.com"}, - {accountID: 3, login: "email3@test.com"}, - {accountID: 4, login: "email4@test.com"}, - ] + {accountID: 1, login: 'email1@test.com'}, + {accountID: 2, login: 'email2@test.com'}, + {accountID: 3, login: 'email3@test.com'}, + {accountID: 4, login: 'email4@test.com'}, + ]; const eightParticipants = [ - {accountID: 1, login: "email1@test.com"}, - {accountID: 2, login: "email2@test.com"}, - {accountID: 3, login: "email3@test.com"}, - {accountID: 4, login: "email4@test.com"}, - {accountID: 5, login: "email5@test.com"}, - {accountID: 6, login: "email6@test.com"}, - {accountID: 7, login: "email7@test.com"}, - {accountID: 8, login: "email8@test.com"}, - ] + {accountID: 1, login: 'email1@test.com'}, + {accountID: 2, login: 'email2@test.com'}, + {accountID: 3, login: 'email3@test.com'}, + {accountID: 4, login: 'email4@test.com'}, + {accountID: 5, login: 'email5@test.com'}, + {accountID: 6, login: 'email6@test.com'}, + {accountID: 7, login: 'email7@test.com'}, + {accountID: 8, login: 'email8@test.com'}, + ]; describe('When participantAccountIDs is passed to getGroupChatName', () => { it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { From 8768859e04357c3bfd0992c30de0e21c2d7d5605 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 20 Sep 2024 09:42:33 +0700 Subject: [PATCH 008/421] fix: deleted workspace with invoices is accessible by url --- src/pages/workspace/WorkspaceInitialPage.tsx | 11 ++++++----- .../workspace/WorkspacePageWithSections.tsx | 19 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index fd7a45e31acb..7f51af6192a5 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -94,6 +94,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors)); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); const hasSyncError = PolicyUtils.hasSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy)); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); @@ -306,11 +307,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = - isEmptyObject(policy) || - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); + const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); useEffect(() => { if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) { @@ -360,7 +361,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories onBackButtonPress={Navigation.dismissModal} onLinkPress={Navigation.resetToHome} shouldShow={shouldShowNotFoundPage} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} > fetchData(policyID, shouldSkipVBBACall)}); + const {isOffline} = useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; @@ -148,7 +149,6 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); - const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -161,19 +161,18 @@ function WorkspacePageWithSections({ }, [policyID, shouldSkipVBBACall]), ); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { return true; } - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - return ( - (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)) - ); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace + return (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [policy, shouldShowNonAdmin]); + }, [policy, shouldShowNonAdmin, shouldShowPolicy, prevShouldShowPolicy]); return ( Date: Sat, 28 Sep 2024 01:33:25 +0700 Subject: [PATCH 009/421] use prevPolicy --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- src/pages/workspace/WorkspacePageWithSections.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 33330be8d9fb..156282c9f281 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -298,7 +298,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index fec440da970a..cf473ebec0ba 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -139,6 +139,7 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); + const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -152,7 +153,7 @@ function WorkspacePageWithSections({ ); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { From 093f361d0cee939407b5872c889ddc565ff85881 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 28 Sep 2024 01:33:41 +0700 Subject: [PATCH 010/421] remove redundant changes --- src/pages/workspace/WorkspacePageWithSections.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index cf473ebec0ba..26175c9793d9 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -140,7 +140,6 @@ function WorkspacePageWithSections({ const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); - useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true firstRender.current = false; From ac0f5b90eda70154b6612310766f1cc394678458 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 2 Oct 2024 05:55:42 +0530 Subject: [PATCH 011/421] =?UTF-8?q?feat:=20Implement=20to=20use=20a=20?= =?UTF-8?q?=F0=9F=91=8Dicon=20next=20to=20approved=20report=20preview.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 87f06f43d82a..94755ebb6944 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -145,12 +145,18 @@ function ReportPreview({ transform: [{scale: checkMarkScale.value}], })); + const isApproved = ReportUtils.isReportApproved(iouReport, action); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpStyle = useAnimatedStyle(() => ({ + ...styles.defaultCheckmarkWrapper, + transform: [{scale: thumbsUpScale.value}], + })); + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); - const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const numberOfRequests = allTransactions.length; const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); @@ -433,6 +439,14 @@ function ReportPreview({ } }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); + useEffect(() => { + if (!isApproved) { + return; + } + + thumbsUpScale.value = withSpring(1, {duration: 200}); + }, [isApproved, thumbsUpScale]); + return ( )} + {isApproved && ( + + + + )} {shouldShowSubtitle && supportText && ( From beb006f9e68dfc560e39183a219a337937f7e7d8 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 16:35:53 +0530 Subject: [PATCH 012/421] fix: Room - Create room whisper reappears when interacting with it after workspace is deleted. Signed-off-by: krishna2323 --- .../AttachmentCarousel/extractAttachments.ts | 5 +++-- .../AttachmentCarousel/index.native.tsx | 4 ++-- .../Attachments/AttachmentCarousel/index.tsx | 18 +++++++++++++++--- src/components/ParentNavigationSubtitle.tsx | 2 +- src/libs/OptionsListUtils.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 19 ++++++++++++++----- src/libs/SidebarUtils.ts | 2 +- src/pages/home/ReportScreen.tsx | 5 ++++- src/pages/home/report/ReportActionsList.tsx | 4 ++-- 9 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 81ee6d08934b..69b0b8229f4a 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -19,7 +19,8 @@ function extractAttachments( accountID, parentReportAction, reportActions, - }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, + reportID, + }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; reportID: string}, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; @@ -95,7 +96,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index a8eb614202a7..9aa619eb1cda 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -34,9 +34,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, reportID: report.reportID}); } let newIndex = newAttachments.findIndex(compareImage); diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index a1408aaf400e..ac4975d85665 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -76,9 +76,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, reportID: report.reportID}); } if (isEqual(attachments, newAttachments)) { @@ -117,7 +117,19 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi onNavigate(attachment); } } - }, [report.privateNotes, reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, type]); + }, [ + report.privateNotes, + reportActions, + parentReportActions, + compareImage, + report.parentReportActionID, + attachments, + setDownloadButtonVisibility, + onNavigate, + accountID, + type, + report.reportID, + ]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 997106f3e649..ef0f981a8c77 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -39,7 +39,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); if (isVisibleAction && !isOffline) { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fbf2f3b94c7c..2e19d5a9538f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -327,7 +327,7 @@ Onyx.connect({ // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => - ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID) && !ReportActionUtils.isWhisperAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && @@ -677,7 +677,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '-1']?.find( (reportAction, key): reportAction is ReportAction => - ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, key, reportID) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3b5e0a8eeaa3..105fbee48c31 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -27,6 +27,7 @@ import Parser from './Parser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; +import * as ReportUtils from './ReportUtils'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle @@ -630,7 +631,15 @@ const supportedActionTypes: ReportActionName[] = [...Object.values(otherActionTy * Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid * and supported type, it's not deleted and also not closed. */ -function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number): boolean { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string): boolean { + const report = ReportUtils.getReport(reportID); + if ( + (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && + !ReportUtils.canUserPerformWriteAction(report) + ) { + return false; + } + if (!reportAction) { return false; } @@ -706,7 +715,7 @@ function isResolvedActionTrackExpense(reportAction: OnyxEntry): bo * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. */ -function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry): boolean { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, reportID: string): boolean { if (!reportAction) { return false; } @@ -718,7 +727,7 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry< // If a whisper action is the REPORT_PREVIEW action, we are displaying it. // If the action's message text is empty and it is not a deleted parent with visible child actions, hide it. Else, consider the action to be displayable. return ( - shouldReportActionBeVisible(reportAction, reportAction.reportActionID) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, reportID) && !(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) && !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction)) && !isResolvedActionTrackExpense(reportAction) @@ -760,7 +769,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, reportID)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -1087,7 +1096,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn */ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true)); - const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index eb5b3c58cdef..4c2a245af9dc 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -46,7 +46,7 @@ Onyx.connect({ // The report is only visible if it is the last action not deleted that // does not match a closed or created state. const reportActionsForDisplay = actionsArray.filter( - (reportAction) => ReportActionsUtils.shouldReportActionBeVisibleAsLastAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + (reportAction) => ReportActionsUtils.shouldReportActionBeVisibleAsLastAction(reportAction, reportID) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, ); const reportAction = reportActionsForDisplay.at(-1); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 8afeb0cf2307..56545e80aeae 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -335,7 +335,10 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ? reportActions.length > 0 : reportActions.length >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); - const isLinkedActionDeleted = useMemo(() => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID), [linkedAction]); + const isLinkedActionDeleted = useMemo( + () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1'), + [linkedAction, report?.reportID], + ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index ce925d4375af..6025fe64ffb2 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -193,9 +193,9 @@ function ReportActionsList({ ReportActionsUtils.isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID), + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID), ), - [sortedReportActions, isOffline], + [sortedReportActions, isOffline, report.reportID], ); /** From 921f47429960cd6b5f1110e50f5bb7139a6736e2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 16:43:39 +0530 Subject: [PATCH 013/421] fix lint issues. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 105fbee48c31..8681980163e6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,7 +28,6 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; -import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -123,7 +122,7 @@ function isCreatedAction(reportAction: OnyxInputOrEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { +function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { const message = reportAction?.message ?? []; if (!Array.isArray(message)) { @@ -136,7 +135,7 @@ function isDeletedAction(reportAction: OnyxInputOrEntry): bo return (getReportActionMessage(reportAction)?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isReversedTransaction(reportAction: OnyxInputOrEntry) { +function isReversedTransaction(reportAction: OnyxInputOrEntry) { return (getReportActionMessage(reportAction)?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } @@ -360,7 +359,7 @@ function getParentReportAction(report: OnyxInputOrEntry): OnyxEntry): boolean { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && @@ -990,11 +989,11 @@ function isSplitBillAction(reportAction: OnyxInputOrEntry): report return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } -function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { +function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; } -function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { +function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY; } @@ -1204,7 +1203,7 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } -function getReportActionHtml(reportAction: PartialReportAction): string { +function getReportActionHtml(reportAction: ReportUtils.PartialReportAction): string { return getReportActionMessage(reportAction)?.html ?? ''; } @@ -1220,7 +1219,7 @@ function getTextFromHtml(html?: string): string { return html ? Parser.htmlToText(html) : ''; } -function isOldDotLegacyAction(action: OldDotReportAction | PartialReportAction): action is PartialReportAction { +function isOldDotLegacyAction(action: OldDotReportAction | ReportUtils.PartialReportAction): action is ReportUtils.PartialReportAction { return [ CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -1260,7 +1259,7 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { ].some((oldDotActionName) => oldDotActionName === action.actionName); } -function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { +function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } @@ -1275,7 +1274,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { /** * Helper method to format message of OldDot Actions. */ -function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldDotReportAction, withMarkdown = true): string { +function getMessageOfOldDotReportAction(oldDotAction: ReportUtils.PartialReportAction | OldDotReportAction, withMarkdown = true): string { if (isOldDotLegacyAction(oldDotAction)) { return getMessageOfOldDotLegacyAction(oldDotAction); } From 597e24318607e9d089961b35b9d008ceef0c9894 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 17:58:44 +0530 Subject: [PATCH 014/421] minor update. Signed-off-by: krishna2323 --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/hooks/usePaginatedReportActions.ts | 2 +- src/libs/Middleware/Pagination.ts | 4 +-- src/libs/ReportActionsUtils.ts | 31 ++++++++++--------- src/libs/actions/Report.ts | 2 +- src/pages/Debug/Report/DebugReportActions.tsx | 2 +- .../report/ReportActionItemParentAction.tsx | 6 +++- src/pages/home/report/ReportActionsView.tsx | 2 +- src/pages/home/report/ThreadDivider.tsx | 2 +- .../perf-test/ReportActionsUtils.perf-test.ts | 4 +-- tests/unit/ReportActionsUtilsTest.ts | 6 ++-- 11 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 08240a211804..b317d3020e2a 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -139,7 +139,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio : '-1'; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index a32d4f7d3dd0..e3525dcf91f5 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -14,7 +14,7 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index bfa8183ac03b..7b3abdee849c 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -15,7 +15,7 @@ type PagedResource = OnyxValues[TResourc type PaginationCommonConfig = { resourceCollectionKey: TResourceKey; pageCollectionKey: TPageKey; - sortItems: (items: OnyxValues[TResourceKey]) => Array>; + sortItems: (items: OnyxValues[TResourceKey], reportID: string) => Array>; getItemID: (item: PagedResource) => string; }; @@ -96,7 +96,7 @@ const Pagination: Middleware = (requestResponse, request) => { // Create a new page based on the response const pageItems = (response.onyxData.find((data) => data.key === resourceKey)?.value ?? {}) as OnyxValues[typeof resourceCollectionKey]; - const sortedPageItems = sortItems(pageItems); + const sortedPageItems = sortItems(pageItems, resourceID); if (sortedPageItems.length === 0) { // Must have at least 1 action to create a page. Log.hmmm(`[Pagination] Did not receive any items in the response to ${request.command}`); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8681980163e6..8a1ff9431e28 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -27,7 +27,8 @@ import Parser from './Parser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; -import * as ReportUtils from './ReportUtils'; +import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; +import {canUserPerformWriteAction, getReport} from './ReportUtils.ts'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -122,7 +123,7 @@ function isCreatedAction(reportAction: OnyxInputOrEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { +function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { const message = reportAction?.message ?? []; if (!Array.isArray(message)) { @@ -135,7 +136,7 @@ function isDeletedAction(reportAction: OnyxInputOrEntry): bo return (getReportActionMessage(reportAction)?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isReversedTransaction(reportAction: OnyxInputOrEntry) { +function isReversedTransaction(reportAction: OnyxInputOrEntry) { return (getReportActionMessage(reportAction)?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } @@ -359,7 +360,7 @@ function getParentReportAction(report: OnyxInputOrEntry): OnyxEntry): boolean { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && @@ -631,10 +632,10 @@ const supportedActionTypes: ReportActionName[] = [...Object.values(otherActionTy * and supported type, it's not deleted and also not closed. */ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string): boolean { - const report = ReportUtils.getReport(reportID); + const report = getReport(reportID); if ( (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && - !ReportUtils.canUserPerformWriteAction(report) + !canUserPerformWriteAction(report) ) { return false; } @@ -834,7 +835,7 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: OnyxEntry | ReportAction[], reportID: string, shouldIncludeInvisibleActions = false): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -844,7 +845,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry shouldReportActionBeVisible(reportAction, key)) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, reportID)) .map(([, reportAction]) => reportAction); } @@ -989,11 +990,11 @@ function isSplitBillAction(reportAction: OnyxInputOrEntry): report return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } -function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { +function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; } -function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { +function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY; } @@ -1203,7 +1204,7 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } -function getReportActionHtml(reportAction: ReportUtils.PartialReportAction): string { +function getReportActionHtml(reportAction: PartialReportAction): string { return getReportActionMessage(reportAction)?.html ?? ''; } @@ -1219,7 +1220,7 @@ function getTextFromHtml(html?: string): string { return html ? Parser.htmlToText(html) : ''; } -function isOldDotLegacyAction(action: OldDotReportAction | ReportUtils.PartialReportAction): action is ReportUtils.PartialReportAction { +function isOldDotLegacyAction(action: OldDotReportAction | PartialReportAction): action is PartialReportAction { return [ CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -1259,7 +1260,7 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { ].some((oldDotActionName) => oldDotActionName === action.actionName); } -function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportAction) { +function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } @@ -1274,7 +1275,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportA /** * Helper method to format message of OldDot Actions. */ -function getMessageOfOldDotReportAction(oldDotAction: ReportUtils.PartialReportAction | OldDotReportAction, withMarkdown = true): string { +function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldDotReportAction, withMarkdown = true): string { if (isOldDotLegacyAction(oldDotAction)) { return getMessageOfOldDotLegacyAction(oldDotAction); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 13b14d380758..7ac929b9fad0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -274,7 +274,7 @@ registerPaginationConfig({ nextCommand: READ_COMMANDS.GET_NEWER_ACTIONS, resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + sortItems: (reportActions, reportID) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, true), getItemID: (reportAction) => reportAction.reportActionID, }); diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index e7c4059fffe7..d25c9175a4b3 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -23,7 +23,7 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { const styles = useThemeStyles(); const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), }); const renderItem = ({item}: ListRenderItemInfo) => ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + ); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); if (isVisibleAction && !isOffline) { diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 8896611905ca..b58de22bc520 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -90,7 +90,7 @@ function ReportActionsView({ const route = useRoute>(); const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? -1}`, { - selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, true), }); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? -1}`); const prevTransactionThreadReport = usePrevious(transactionThreadReport); diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index d2ffa97f58b2..6fec617e4c37 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -47,7 +47,7 @@ function ThreadDivider({ancestor, isLinkDisabled = false}: ThreadDividerProps) { ) : ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', ancestor.report.reportID); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); if (isVisibleAction && !isOffline) { diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index a33a448cfee7..5e258436edc7 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -93,7 +93,7 @@ describe('ReportActionsUtils', () => { }); test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => { - const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); + const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId); await waitForBatchedUpdates(); await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray)); @@ -132,7 +132,7 @@ describe('ReportActionsUtils', () => { test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions)); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index d753069265f8..b6e29e89d025 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -302,7 +302,7 @@ describe('ReportActionsUtils', () => { }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); expect(result).toStrictEqual(input); }); @@ -392,7 +392,7 @@ describe('ReportActionsUtils', () => { ], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); input.pop(); expect(result).toStrictEqual(input); }); @@ -437,7 +437,7 @@ describe('ReportActionsUtils', () => { message: [{html: '', type: 'Action type', text: 'Action text'}], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); input.pop(); expect(result).toStrictEqual(input); }); From 352abf82c0e2c577133871a18813af3189dbe22b Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 17:59:42 +0530 Subject: [PATCH 015/421] minor update. Signed-off-by: krishna2323 --- src/libs/Middleware/Pagination.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 7b3abdee849c..251609d1254c 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -115,7 +115,7 @@ const Pagination: Middleware = (requestResponse, request) => { const resourceCollections = resources.get(resourceCollectionKey) ?? {}; const existingItems = resourceCollections[resourceKey] ?? {}; const allItems = fastMerge(existingItems, pageItems, true); - const sortedAllItems = sortItems(allItems); + const sortedAllItems = sortItems(allItems, resourceID); const pagesCollections = pages.get(pageCollectionKey) ?? {}; const existingPages = pagesCollections[pageKey] ?? []; From 534c19e7e5fbd9c934bd725ef5570c9525012530 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 21:44:24 +0530 Subject: [PATCH 016/421] fix lint issue. Signed-off-by: krishna2323 --- src/hooks/usePaginatedReportActions.ts | 2 +- src/libs/ReportActionsUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index e3525dcf91f5..342d73b08bd8 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -14,7 +14,7 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8a1ff9431e28..0807f8f95ed7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,7 +28,7 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; -import {canUserPerformWriteAction, getReport} from './ReportUtils.ts'; +import {canUserPerformWriteAction, getReport} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; From 51842c47fb01dc9373f19ef7537aec5c20d88a35 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 20 Oct 2024 18:03:09 +0530 Subject: [PATCH 017/421] minor updates. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 6c87d9f3d559..5097d34111c7 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], @@ -483,7 +483,7 @@ function ReportPreview({ - {previewMessage} + {previewMessage} {shouldShowRBR && ( Date: Sun, 20 Oct 2024 18:07:14 +0530 Subject: [PATCH 018/421] make animation subtle. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5097d34111c7..9ebcf792ed5e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], From eee882cb68a3299a1d348997db5bbebea1cf1bcd Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 11:08:17 +0700 Subject: [PATCH 019/421] fix: show video control when video ended --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 012537b75108..1970a2692e90 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -70,6 +70,7 @@ function BaseVideoPlayer({ const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [isEnded, setIsEnded] = useState(false); const [isBuffering, setIsBuffering] = useState(true); // we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning const [sourceURL] = useState(VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001)); @@ -199,6 +200,8 @@ function BaseVideoPlayer({ return; } + setIsEnded(status.didJustFinish && !status.isLooping); + if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); } @@ -456,7 +459,7 @@ function BaseVideoPlayer({ {((isLoading && !isOffline) || (isBuffering && !isPlaying)) && } {isLoading && (isOffline || !isBuffering) && } - {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( + {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen || isEnded) && ( Date: Tue, 22 Oct 2024 11:43:00 +0700 Subject: [PATCH 020/421] fix: sorted suggestion emoji --- src/libs/EmojiUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 7c042bbefe67..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,4 +1,5 @@ import {Str} from 'expensify-common'; +import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; @@ -424,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -434,7 +435,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -442,7 +443,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } /** From 596f16f64f64b75595f29efb76e0cea93c7d891e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:34:55 +0700 Subject: [PATCH 021/421] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..bf5d611b1a73 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return matching; } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, From 313d716179a0f588b114cefab506ef5043a64f0d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:35:19 +0700 Subject: [PATCH 022/421] chore --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From 8e6b36067b820412f56f7d6a95c2315affbd08d3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:47:59 +0700 Subject: [PATCH 023/421] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- tests/unit/EmojiTest.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index bf5d611b1a73..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index c96228b49fbc..2033085c5694 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -154,6 +154,11 @@ describe('EmojiTest', () => { it('correct suggests emojis accounting for keywords', () => { const thumbEmojisEn: Emoji[] = [ + { + name: 'hand_with_index_finger_and_thumb_crossed', + code: '🫰', + types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], + }, { code: '👍', name: '+1', @@ -164,11 +169,6 @@ describe('EmojiTest', () => { name: '-1', types: ['👎🏿', '👎🏾', '👎🏽', '👎🏼', '👎🏻'], }, - { - name: 'hand_with_index_finger_and_thumb_crossed', - code: '🫰', - types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], - }, ]; const thumbEmojisEs: Emoji[] = [ From 7cacc00521af3c4b7bd97df155db2fab95e075da Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:49:51 +0700 Subject: [PATCH 024/421] fix lint --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From b7a9a4fb56e464a943fc2b82fda9d2b56a3c4422 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 24 Oct 2024 14:10:45 -0700 Subject: [PATCH 025/421] Conditionally add nvp_onboarding when not invited --- src/libs/actions/IOU.ts | 2 +- src/libs/actions/Report.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 497f43f93317..531f8176021a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7604,7 +7604,7 @@ function completePaymentOnboarding(paymentSelected: ValueOf, full = true) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index dce8f2d19559..45fd68df1be7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3329,6 +3329,7 @@ function completeOnboarding( paymentSelected?: string, companySize?: OnboardingCompanySizeType, userReportedIntegration?: OnboardingAccountingType, + wasInvited = false, ) { const actorAccountID = CONST.ACCOUNT_ID.CONCIERGE; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID, currentUserAccountID]); @@ -3574,12 +3575,14 @@ function completeOnboarding( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: engagementChoice}, }, - { + ); + if (!wasInvited) { + optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_ONBOARDING, value: {hasCompletedGuidedSetupFlow: true}, - }, - ); + }); + } const successData: OnyxUpdate[] = [...tasksForSuccessData]; successData.push({ From 791649a506ef26c28ce729be918f762468ef350c Mon Sep 17 00:00:00 2001 From: truph01 Date: Fri, 25 Oct 2024 06:14:34 +0700 Subject: [PATCH 026/421] fix: incorrect message displayed when deleting workspace --- src/pages/workspace/WorkspacesListPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 82503134b09e..f3cbdd388e21 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -124,7 +124,11 @@ function WorkspacesListPage() { const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyIDToDelete ?? '-1'); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); - const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList); + const hasCardFeedOrExpensifyCard = + !isEmptyObject(cardFeeds) || + !isEmptyObject(cardsList) || + ((PolicyUtils.getPolicy(policyIDToDelete)?.areExpensifyCardsEnabled || PolicyUtils.getPolicy(policyIDToDelete)?.areCompanyCardsEnabled) && + PolicyUtils.getPolicy(policyIDToDelete)?.workspaceAccountID); const confirmDeleteAndHideModal = () => { if (!policyIDToDelete || !policyNameToDelete) { From 45e8f104b299128b7a565da3fbb6bff70a81fb9c Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 24 Oct 2024 16:15:57 -0700 Subject: [PATCH 027/421] Add undefined params to get pamentSeleted in right place; wasinvited too --- src/libs/actions/IOU.ts | 13 ++++++++++++- src/libs/actions/Report.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 531f8176021a..7d0281a43b21 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7604,7 +7604,18 @@ function completePaymentOnboarding(paymentSelected: ValueOf, full = true) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 45fd68df1be7..6e5ecf7f53a6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3329,7 +3329,7 @@ function completeOnboarding( paymentSelected?: string, companySize?: OnboardingCompanySizeType, userReportedIntegration?: OnboardingAccountingType, - wasInvited = false, + wasInvited?: boolean, ) { const actorAccountID = CONST.ACCOUNT_ID.CONCIERGE; const targetChatReport = ReportUtils.getChatByParticipants([actorAccountID, currentUserAccountID]); From 9935cc3d8449f8a71c80522eff09a0defceedcd7 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 24 Oct 2024 16:27:47 -0700 Subject: [PATCH 028/421] Apply same reasoning to failure data --- src/libs/actions/Report.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6e5ecf7f53a6..84b438d4a2fa 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3637,6 +3637,9 @@ function completeOnboarding( key: ONYXKEYS.NVP_INTRO_SELECTED, value: {choice: null}, }, + ); + + if (!wasInvited) failureData.push( { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_ONBOARDING, From 38d75c4255ff953f1188061704251bf18fe69749 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 24 Oct 2024 17:17:21 -0700 Subject: [PATCH 029/421] Fix bad syntax --- src/libs/actions/Report.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 84b438d4a2fa..91eec9df1d77 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3639,13 +3639,13 @@ function completeOnboarding( }, ); - if (!wasInvited) failureData.push( - { + if (!wasInvited) { + failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_ONBOARDING, value: {hasCompletedGuidedSetupFlow: false}, - }, - ); + }); + } if (userReportedIntegration) { optimisticData.push({ From 33d1ba31a3d0f1ec31e749eba325ed03c66918b2 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:59:08 +0530 Subject: [PATCH 030/421] Update --- tests/ui/GroupChatNameTests.tsx | 4 ---- tests/utils/TestHelper.ts | 29 +++++------------------------ 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 25defa2ff283..393cf2c6dad2 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -41,10 +41,6 @@ let transitionEndCB: () => void; jest.mock('@react-navigation/native'); -beforeAll(() => { - TestHelper.beforeAllSetupUITests(); -}); - const REPORT_ID = '1'; const USER_A_ACCOUNT_ID = 1; const USER_A_EMAIL = 'user_a@test.com'; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 084edc1d1567..394c05fe8b7c 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -313,30 +313,12 @@ function assertFormDataMatchesObject(obj: Report, formData?: FormData) { async function navigateToSidebarOption(index: number): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); - const optionRows = screen.queryAllByAccessibilityHint(hintText); - fireEvent(optionRows[index], 'press'); - await waitForBatchedUpdatesWithAct(); -} - -function beforeAllSetupUITests(shouldConnectToPusher = false) { - // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it - // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means - // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling - // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to - // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. - global.fetch = getGlobalFetchMock(); - - Linking.setInitialURL('https://new.expensify.com/'); - appSetup(); - - if (shouldConnectToPusher) { - PusherConnectionManager.init(); - Pusher.init({ - appKey: CONFIG.PUSHER.APP_KEY, - cluster: CONFIG.PUSHER.CLUSTER, - authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, - }); + const optionRow = screen.queryAllByAccessibilityHint(hintText).at(index); + if (!optionRow) { + return; } + fireEvent(optionRow, 'press'); + await waitForBatchedUpdatesWithAct(); } export type {MockFetch, FormData}; @@ -353,5 +335,4 @@ export { expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, navigateToSidebarOption, - beforeAllSetupUITests, }; From 0abb380ce52148fba4eea4d2a64f2a3b0e6e0c61 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 29 Oct 2024 10:51:25 +0700 Subject: [PATCH 031/421] fix: Deleted room is displayed in the 'Assign task' --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index a49b474b185e..329066b00a99 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -189,9 +189,10 @@ function FloatingActionButtonAndPopover( const {canUseSpotnanaTravel, canUseCombinedTrackSubmit} = usePermissions(); const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection, session?.email), [allPolicies, session?.email]); + const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport, reportNameValuePairs)); const quickActionAvatars = useMemo(() => { - if (quickActionReport) { + if (isValidReport) { const avatars = ReportUtils.getIcons(quickActionReport, personalDetails); return avatars.length <= 1 || ReportUtils.isPolicyExpenseChat(quickActionReport) ? avatars : avatars.filter((avatar) => avatar.id !== session?.accountID); } @@ -223,7 +224,7 @@ function FloatingActionButtonAndPopover( }, [quickAction, translate, quickActionAvatars, quickActionReport]); const hideQABSubtitle = useMemo(() => { - if (isEmptyObject(quickActionReport)) { + if (isValidReport) { return true; } if (quickActionAvatars.length === 0) { @@ -231,7 +232,7 @@ function FloatingActionButtonAndPopover( } const displayName = personalDetails?.[quickActionAvatars.at(0)?.id ?? -1]?.firstName ?? ''; return quickAction?.action === CONST.QUICK_ACTIONS.SEND_MONEY && displayName.length === 0; - }, [personalDetails, quickActionReport, quickAction?.action, quickActionAvatars]); + }, [isValidReport, quickActionAvatars, personalDetails, quickAction?.action]); const navigateToQuickAction = () => { const selectOption = (onSelected: () => void, shouldRestrictAction: boolean) => { @@ -243,7 +244,6 @@ function FloatingActionButtonAndPopover( onSelected(); }; - const isValidReport = !(isEmptyObject(quickActionReport) || ReportUtils.isArchivedRoom(quickActionReport, reportNameValuePairs)); const quickActionReportID = isValidReport ? quickActionReport?.reportID ?? '-1' : ReportUtils.generateReportID(); switch (quickAction?.action) { From 5889c08cc90ef34eb83e6d42b7922db32e4785e4 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 29 Oct 2024 17:29:05 +0700 Subject: [PATCH 032/421] fix eslint --- .../FloatingActionButtonAndPopover.tsx | 86 ++++--------------- 1 file changed, 15 insertions(+), 71 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 329066b00a99..b8b76e5a00eb 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,10 +1,10 @@ import {useIsFocused as useIsFocusedOriginal, useNavigationState} from '@react-navigation/native'; import type {ImageContentFit} from 'expo-image'; -import type {ForwardedRef, RefAttributes} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -39,6 +39,7 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; // On small screen we hide the search page from central pane to show the search bottom tab page with bottom tab bar. // We need to take this in consideration when checking if the screen is focused. @@ -51,33 +52,7 @@ const useIsFocused = () => { type PolicySelector = Pick; -type FloatingActionButtonAndPopoverOnyxProps = { - /** The list of policies the user has access to. */ - allPolicies: OnyxCollection; - - /** Whether app is in loading state */ - isLoading: OnyxEntry; - - /** Information on the last taken action to display as Quick Action */ - quickAction: OnyxEntry; - - /** The report data of the quick action */ - quickActionReport: OnyxEntry; - - /** The policy data of the quick action */ - quickActionPolicy: OnyxEntry; - - /** The current session */ - session: OnyxEntry; - - /** Personal details of all the users */ - personalDetails: OnyxEntry; - - /** Has user seen track expense training interstitial */ - hasSeenTrackTraining: OnyxEntry; -}; - -type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxProps & { +type FloatingActionButtonAndPopoverProps = { /* Callback function when the menu is shown */ onShowCreateMenu?: () => void; @@ -161,23 +136,18 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * Responsible for rendering the {@link PopoverMenu}, and the accompanying * FAB that can open or close the menu. */ -function FloatingActionButtonAndPopover( - { - onHideCreateMenu, - onShowCreateMenu, - isLoading = false, - allPolicies, - quickAction, - quickActionReport, - quickActionPolicy, - session, - personalDetails, - hasSeenTrackTraining, - }: FloatingActionButtonAndPopoverProps, - ref: ForwardedRef, -) { +function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector)}); + const [isLoading] = useOnyx(ONYXKEYS.IS_LOADING_APP); + const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); + const [quickActionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`); + const [quickActionPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [hasSeenTrackTraining] = useOnyx(ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${quickActionReport?.reportID ?? -1}`); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); @@ -510,32 +480,6 @@ function FloatingActionButtonAndPopover( FloatingActionButtonAndPopover.displayName = 'FloatingActionButtonAndPopover'; -export default withOnyx, FloatingActionButtonAndPopoverOnyxProps>({ - allPolicies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - }, - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, - quickAction: { - key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, - }, - quickActionReport: { - key: ({quickAction}) => `${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, - }, - quickActionPolicy: { - key: ({quickActionReport}) => `${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - hasSeenTrackTraining: { - key: ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING, - }, -})(forwardRef(FloatingActionButtonAndPopover)); +export default forwardRef(FloatingActionButtonAndPopover); export type {PolicySelector}; From 304f0d2fc357e24e88006c0a35f821dc1f1639c1 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:05:35 +0530 Subject: [PATCH 033/421] Update --- tests/ui/GroupChatNameTests.tsx | 3 +++ tests/utils/LHNTestUtils.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 393cf2c6dad2..3c13605925c9 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -41,6 +41,8 @@ let transitionEndCB: () => void; jest.mock('@react-navigation/native'); +TestHelper.setupApp(); + const REPORT_ID = '1'; const USER_A_ACCOUNT_ID = 1; const USER_A_EMAIL = 'user_a@test.com'; @@ -69,6 +71,7 @@ function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Pro const participants: Record = {}; participantAccountIDs?.forEach((id) => { participants[id] = { + notificationPreference: 'always', hidden: false, role: id === 1 ? CONST.REPORT.ROLE.ADMIN : CONST.REPORT.ROLE.MEMBER, } as Participant; diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 7a9873483069..02223d36f3c6 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -134,7 +134,7 @@ function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0 adminIDs.forEach((id) => { participants[id] = { - hidden: false, + notificationPreference: 'always', role: CONST.REPORT.ROLE.ADMIN, }; }); From e24e1ccd2c10cc2fff1f3addc8240f429d4f8d51 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:50:05 +0530 Subject: [PATCH 034/421] Update --- __mocks__/@react-native-reanimated/index.ts | 11 ----------- tests/ui/GroupChatNameTests.tsx | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 __mocks__/@react-native-reanimated/index.ts diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts deleted file mode 100644 index df9cc4ecef8d..000000000000 --- a/__mocks__/@react-native-reanimated/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -const actualAnimated = jest.requireActual('react-native-reanimated/mock'); - -const mock = { - ...actualAnimated, - createAnimatedPropAdapter: jest.fn(), - useReducedMotion: jest.fn(), -}; - -export default mock; diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 3c13605925c9..919279ad00e3 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -174,7 +174,7 @@ describe('Tests for group chat name', () => { return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D')); })); - it('Should show all 8 names in LHN when 8 participants are present', () => + it('Should show limited names in LHN when 8 participants are present', () => signInAndGetApp('', participantAccountIDs8).then(() => { // Verify the sidebar links are rendered const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); @@ -189,7 +189,7 @@ describe('Tests for group chat name', () => { const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameText = screen.queryByLabelText(displayNameHintText); - return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H')); + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E')); })); it('Check if group name shows fine for report header', () => From cf582b458f571ed64722c10056b3b86fd797ed0f Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:15:52 +0700 Subject: [PATCH 035/421] fix: Update second Allow location access modal on web --- src/components/LocationPermissionModal/index.tsx | 15 ++++++++++----- src/components/LocationPermissionModal/types.ts | 4 +--- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index 0e500a9b7cc4..fcb7cdacbd4c 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -39,10 +39,10 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (hasError) { if (Linking.openSettings) { Linking.openSettings(); + } else { + onDeny?.(); } setShowModal(false); - setHasError(false); - resetPermissionFlow(); return; } cb(); @@ -54,7 +54,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { onGrant(); } else { - onDeny(status); + onDeny(); } }) .finally(() => { @@ -64,7 +64,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; @@ -83,13 +83,17 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }; return ( { + setHasError(false); + resetPermissionFlow(); + }} isVisible={showModal} onConfirm={grantLocationPermission} onCancel={skipLocationPermission} onBackdropPress={closeModal} confirmText={getConfirmText()} cancelText={translate('common.notNow')} - prompt={translate(hasError ? 'receipt.locationErrorMessage' : 'receipt.locationAccessMessage')} promptStyles={[styles.textLabelSupportingEmptyValue, styles.mb4]} title={translate(hasError ? 'receipt.locationErrorTitle' : 'receipt.locationAccessTitle')} titleContainerStyles={[styles.mt2, styles.mb0]} @@ -100,6 +104,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe iconHeight={120} shouldCenterIcon shouldReverseStackedButtons + prompt={translate(hasError ? (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage') : 'receipt.locationAccessMessage')} /> ); } diff --git a/src/components/LocationPermissionModal/types.ts b/src/components/LocationPermissionModal/types.ts index ec603bfdb8c1..eb18e1d71c13 100644 --- a/src/components/LocationPermissionModal/types.ts +++ b/src/components/LocationPermissionModal/types.ts @@ -1,11 +1,9 @@ -import type {PermissionStatus} from 'react-native-permissions'; - type LocationPermissionModalProps = { /** A callback to call when the permission has been granted */ onGrant: () => void; /** A callback to call when the permission has been denied */ - onDeny: (permission: PermissionStatus) => void; + onDeny: () => void; /** Should start the permission flow? */ startPermissionFlow: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4c68da68ede6..3b2563e73dda 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -786,6 +786,7 @@ const translations = { locationAccessMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', locationErrorTitle: 'Allow location access', locationErrorMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, dropTitle: 'Let it go', dropMessage: 'Drop your file here', flash: 'flash', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4aed242db5fa..d9b0e96e0390 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,6 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 379f9c56197048cf04a6d6d74d75f2561c24a6ad Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:23:17 +0700 Subject: [PATCH 036/421] fix: type --- src/components/LocationPermissionModal/index.android.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx index 30896cf37084..6e4e6877c540 100644 --- a/src/components/LocationPermissionModal/index.android.tsx +++ b/src/components/LocationPermissionModal/index.android.tsx @@ -50,7 +50,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setHasError(true); return; } else { - onDeny(status); + onDeny(); } setShowModal(false); setHasError(false); @@ -58,7 +58,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; From a34d504ff4c0e33ea26f5a2a109e34539a124c28 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:37:00 +0700 Subject: [PATCH 037/421] fix: lint --- src/components/LocationPermissionModal/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index fcb7cdacbd4c..45e3f5b22d1b 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Linking} from 'react-native'; import {RESULTS} from 'react-native-permissions'; import ConfirmModal from '@components/ConfirmModal'; @@ -81,6 +81,9 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setShowModal(false); resetPermissionFlow(); }; + + const locationErrorMessage = useMemo(() => (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage'), [isWeb]); + return ( ); } From c0a7a2f31e8b5c5a943720f9cfb045c18a876121 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 16:50:58 +0700 Subject: [PATCH 038/421] create a function to remove dup code --- src/libs/EmojiUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..f9fb5f226280 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -24,6 +24,8 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; +const sortByName = (emoji: Emoji, emojiData: RegExpMatchArray) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1)); + let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; Onyx.connect({ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, @@ -425,7 +427,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -435,7 +437,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -443,7 +445,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } /** From 8206ad0092a999dfd633f5874ec073d042059380 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 30 Oct 2024 17:35:14 +0700 Subject: [PATCH 039/421] fix: sound plays before paying held expense report --- src/components/ProcessMoneyReportHoldMenu.tsx | 2 ++ src/components/SettlementButton/index.tsx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 3d6ad9006dc5..f1a72cc7fb8e 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -4,6 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import Navigation from '@libs/Navigation/Navigation'; import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -74,6 +75,7 @@ function ProcessMoneyReportHoldMenu({ if (startAnimation) { startAnimation(); } + playSound(SOUNDS.SUCCESS); IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full); } onClose(); diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 53b09bfcbf31..f98415a5f11a 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -208,7 +208,9 @@ function SettlementButton({ return; } - playSound(SOUNDS.SUCCESS); + if (!ReportUtils.hasHeldExpenses(iouReport?.reportID)) { + playSound(SOUNDS.SUCCESS); + } onPress(iouPaymentType); }; From a8b9955c47368b50588b7bbbb27026d71b855e32 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:57:33 +0530 Subject: [PATCH 040/421] Update --- tests/ui/GroupChatNameTests.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 919279ad00e3..fc383efe4e28 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -236,7 +236,7 @@ describe('Tests for group chat name', () => { const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameText = screen.queryByLabelText(displayNameHintText); - expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H'); + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E'); return navigateToSidebarOption(0); }) From 0705ceed3a258cda1fb33e819b662b56539fd78a Mon Sep 17 00:00:00 2001 From: Ugo Giordano <144606237+ugogiordano@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:02:41 +0100 Subject: [PATCH 041/421] Fixing duplicate Concierge chat Addressing the issue of duplicate Concierge chats appearing in the Left Hand Navigation (LHN) when deep linking to Concierge. --- src/pages/ConciergePage.tsx | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index 46f17e76c083..7ab8deef1bef 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -1,9 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, useOnyx,withOnyx} from 'react-native-onyx'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -33,21 +32,23 @@ function ConciergePage({session}: ConciergePageProps) { const styles = useThemeStyles(); const isUnmounted = useRef(false); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); - useFocusEffect(() => { - if (session && 'authToken' in session) { - App.confirmReadyToOpenApp(); - // Pop the concierge loading page before opening the concierge report. - Navigation.isNavigationReady().then(() => { - if (isUnmounted.current) { - return; - } - Report.navigateToConciergeChat(true, () => !isUnmounted.current); - }); - } else { - Navigation.navigate(); - } - }); + useFocusEffect( + useCallback(() => { + if (session && 'authToken' in session) { + App.confirmReadyToOpenApp(); + Navigation.isNavigationReady().then(() => { + if (isUnmounted.current || isLoadingReportData === undefined || !!isLoadingReportData) { + return; + } + Report.navigateToConciergeChat(true, () => !isUnmounted.current); + }); + } else { + Navigation.navigate(); + } + }, [session, isLoadingReportData]), + ); useEffect(() => { isUnmounted.current = false; From 4d20eaa6e1b42b0d4e94f21e603cfe0cd42a3fc0 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 23:35:13 +0700 Subject: [PATCH 042/421] fix show video controler in native --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 1970a2692e90..fad641e696ae 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -96,6 +96,7 @@ function BaseVideoPlayer({ const shouldUseNewRate = typeof source === 'number' || !source || source.uri !== sourceURL; const togglePlayCurrentVideo = useCallback(() => { + setIsEnded(false); videoResumeTryNumberRef.current = 0; if (!isCurrentlyURLSet) { updateCurrentlyPlayingURL(url); @@ -107,9 +108,12 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumberRef]); const hideControl = useCallback(() => { + if (isEnded) { + return; + } // eslint-disable-next-line react-compiler/react-compiler controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); - }, [controlsOpacity]); + }, [controlsOpacity, isEnded]); const debouncedHideControl = useMemo(() => debounce(hideControl, 1500), [hideControl]); useEffect(() => { @@ -199,8 +203,11 @@ function BaseVideoPlayer({ onPlaybackStatusUpdate?.(status); return; } - - setIsEnded(status.didJustFinish && !status.isLooping); + if (status.didJustFinish) { + setIsEnded(status.didJustFinish && !status.isLooping); + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); From b64f63db80eb53a9e6dab3c735f7293557daa53b Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:41:41 +0530 Subject: [PATCH 043/421] Update timeout of UnreadIndicatorsTest.tsx --- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index bada60720e36..4cb2b5463fb9 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -30,7 +30,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App -jest.setTimeout(30000); +jest.setTimeout(40000); jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); From bd9f6e8077ce434b1b034c45e195928dc94c81ab Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:42:35 +0530 Subject: [PATCH 044/421] Update timeout of UnreadIndicatorsTest.tsx --- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 4cb2b5463fb9..c201d8cabd74 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -30,7 +30,7 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App -jest.setTimeout(40000); +jest.setTimeout(45000); jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); From e602e696743a5f2554fc5cf4f86872776ac47e93 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 04:56:08 +0530 Subject: [PATCH 045/421] pass 'canUserPerformWriteAction' to shouldReportActionBeVisible. Signed-off-by: krishna2323 --- .../AttachmentCarousel/extractAttachments.ts | 5 +- .../LHNOptionsList/LHNOptionsList.tsx | 4 +- src/components/ParentNavigationSubtitle.tsx | 5 +- src/hooks/usePaginatedReportActions.ts | 5 +- src/libs/OptionsListUtils.ts | 7 +- src/libs/ReportActionsUtils.ts | 33 ++--- src/libs/actions/Report.ts | 21 +++- src/pages/Debug/Report/DebugReportActions.tsx | 5 +- src/pages/home/ReportScreen.tsx | 6 +- .../report/ReportActionItemParentAction.tsx | 113 +++++++++--------- src/pages/home/report/ReportActionsList.tsx | 4 +- src/pages/home/report/ReportActionsView.tsx | 3 +- src/pages/home/report/ThreadDivider.tsx | 8 +- .../perf-test/ReportActionsUtils.perf-test.ts | 8 +- tests/unit/ReportActionsUtilsTest.ts | 6 +- 15 files changed, 137 insertions(+), 96 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 69b0b8229f4a..5ebaf8af673e 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -4,6 +4,7 @@ import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; @@ -24,6 +25,8 @@ function extractAttachments( ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. @@ -96,7 +99,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID) || ReportActionsUtils.isMoneyRequestAction(action)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action),) { return; } diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index b317d3020e2a..c03d7822f9e3 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -19,6 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DraftCommentUtils from '@libs/DraftCommentUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -139,7 +140,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio : '-1'; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID); + + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID, ReportUtils.canUserPerformWriteAction(itemFullReport)); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index ef0f981a8c77..248eaec9c9b1 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -5,6 +5,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; import ROUTES from '@src/ROUTES'; @@ -29,6 +30,8 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct const {workspaceName, reportName} = parentNavigationSubtitleData; const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const report = ReportUtils.getReport(parentReportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) if (!reportName) { @@ -39,7 +42,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID, canUserPerformWriteAction); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); if (isVisibleAction && !isOffline) { diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index 342d73b08bd8..6b476db46d7d 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -2,6 +2,7 @@ import {useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; import PaginationUtils from '@libs/PaginationUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; /** @@ -11,10 +12,12 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { // Use `||` instead of `??` to handle empty string. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportIDWithDefault = reportID || '-1'; + const report = ReportUtils.getReport(reportIDWithDefault); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', canUserPerformWriteAction, true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index df4b75893336..9fd121fdfb5b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -328,11 +328,14 @@ Onyx.connect({ lastReportActions[reportID] = firstReportAction; } + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + // The report is only visible if it is the last action not deleted that // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => - ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID, canUserPerformWriteAction) && !ReportActionUtils.isWhisperAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && @@ -590,7 +593,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '-1']?.find( (reportAction, key): reportAction is ReportAction => - ReportActionUtils.shouldReportActionBeVisible(reportAction, key, reportID) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, key, reportID, ReportUtils.canUserPerformWriteAction(report)) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index be0efc67ce5b..7778cb29d552 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,7 +28,6 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; -import {canUserPerformWriteAction, getReport} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -636,12 +635,8 @@ const supportedActionTypes: ReportActionName[] = [...Object.values(otherActionTy * Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid * and supported type, it's not deleted and also not closed. */ -function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string): boolean { - const report = getReport(reportID); - if ( - (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && - !canUserPerformWriteAction(report) - ) { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string, canUserPerformWriteAction?: boolean): boolean { + if ((isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && !canUserPerformWriteAction) { return false; } @@ -720,7 +715,7 @@ function isResolvedActionTrackExpense(reportAction: OnyxEntry): bo * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. */ -function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, reportID: string): boolean { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, reportID: string, canUserPerformWriteAction?: boolean): boolean { if (!reportAction) { return false; } @@ -732,7 +727,7 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry< // If a whisper action is the REPORT_PREVIEW action, we are displaying it. // If the action's message text is empty and it is not a deleted parent with visible child actions, hide it. Else, consider the action to be displayable. return ( - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, reportID) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, reportID, canUserPerformWriteAction) && !(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) && !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction)) && !isResolvedActionTrackExpense(reportAction) @@ -765,7 +760,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo return updatedReportAction; } -function getLastVisibleAction(reportID: string, actionsToMerge: Record | null> = {}): OnyxEntry { +function getLastVisibleAction(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}): OnyxEntry { let reportActions: Array = []; if (!isEmpty(actionsToMerge)) { reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array< @@ -774,7 +769,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action, reportID)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -796,10 +791,11 @@ function formatLastMessageText(lastMessageText: string) { function getLastVisibleMessage( reportID: string, + canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportAction: OnyxInputOrEntry | undefined = undefined, ): LastVisibleMessage { - const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); const message = getReportActionMessage(lastVisibleAction); if (message && isReportMessageAttachment(message)) { @@ -840,7 +836,12 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], reportID: string, shouldIncludeInvisibleActions = false): ReportAction[] { +function getSortedReportActionsForDisplay( + reportActions: OnyxEntry | ReportAction[], + reportID: string, + canUserPerformWriteAction?: boolean, + shouldIncludeInvisibleActions = false, +): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -850,7 +851,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry shouldReportActionBeVisible(reportAction, key, reportID)) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, reportID, canUserPerformWriteAction)) .map(([, reportAction]) => reportAction); } @@ -1099,9 +1100,9 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely */ -function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { +function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction: boolean, actionsToMerge: ReportActions = {}): boolean { const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true)); - const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c219137e210c..03ad1de54b5d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -279,7 +279,11 @@ registerPaginationConfig({ nextCommand: READ_COMMANDS.GET_NEWER_ACTIONS, resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions, reportID) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, true), + sortItems: (reportActions, reportID) => { + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, canUserPerformWriteAction, true); + }, getItemID: (reportAction) => reportAction.reportActionID, }); @@ -1450,8 +1454,10 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { lastVisibleActionCreated: '', }; const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions as ReportActions); + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions as ReportActions); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, canUserPerformWriteAction, optimisticReportActions as ReportActions); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; optimisticReport = { @@ -1461,7 +1467,6 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { lastActorAccountID, }; } - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const didCommentMentionCurrentUser = ReportActionsUtils.didMessageMentionCurrentUser(reportAction); if (didCommentMentionCurrentUser && reportAction.created === report?.lastMentionedTime) { const reportActionsForReport = allReportActions?.[reportID]; @@ -1592,6 +1597,8 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar /** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { const originalReportID = ReportUtils.getOriginalReportID(reportID, originalReportAction); + const report = ReportUtils.getReport(originalReportID ?? '-1'); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); if (!originalReportID || !originalReportAction) { return; @@ -1657,7 +1664,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, canUserPerformWriteAction, true), }); const renderItem = ({item}: ListRenderItemInfo) => ( = CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1'), - [linkedAction, report?.reportID], + () => + !!linkedAction && + !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1', ReportUtils.canUserPerformWriteAction(report)), + [linkedAction, report], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 98cd0448e047..67e16e5b068c 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -106,61 +106,66 @@ function ReportActionItemParentAction({ {/* eslint-disable-next-line react-compiler/react-compiler */} - {allAncestors.map((ancestor) => ( - Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} - > - - {ReportActionsUtils.isTripPreview(ancestor?.reportAction) ? ( - - - - ) : ( - { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( - ancestor.reportAction, - ancestor.reportAction.reportActionID ?? '-1', - ancestor.report.reportID, - ); - // Pop the thread report screen before navigating to the chat report. - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); - if (isVisibleAction && !isOffline) { - // Pop the chat report screen before navigating to the linked report action. - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1', ancestor.reportAction.reportActionID)); - } - } - : undefined - } - parentReportAction={parentReportAction} - report={ancestor.report} - reportActions={reportActions} - transactionThreadReport={transactionThreadReport} - action={ancestor.reportAction} - displayAsGroup={false} - isMostRecentIOUReportAction={false} - shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} - index={index} - isFirstVisibleReportAction={isFirstVisibleReportAction} - shouldUseThreadDividerLine={shouldUseThreadDividerLine} - hideThreadReplies + {allAncestors.map((ancestor) => { + const ancestorReport = ReportUtils.getReport(ancestor.report.reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(ancestorReport); + return ( + Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} + > + - )} - - ))} + {ReportActionsUtils.isTripPreview(ancestor?.reportAction) ? ( + + + + ) : ( + { + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + canUserPerformWriteAction, + ); + // Pop the thread report screen before navigating to the chat report. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); + if (isVisibleAction && !isOffline) { + // Pop the chat report screen before navigating to the linked report action. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1', ancestor.reportAction.reportActionID)); + } + } + : undefined + } + parentReportAction={parentReportAction} + report={ancestor.report} + reportActions={reportActions} + transactionThreadReport={transactionThreadReport} + action={ancestor.reportAction} + displayAsGroup={false} + isMostRecentIOUReportAction={false} + shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} + index={index} + isFirstVisibleReportAction={isFirstVisibleReportAction} + shouldUseThreadDividerLine={shouldUseThreadDividerLine} + hideThreadReplies + /> + )} + + ); + })} {shouldDisplayReplyDivider && } ); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 22eda76df1de..95ddd3f561f5 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -195,9 +195,9 @@ function ReportActionsList({ ReportActionsUtils.isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID), + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID, ReportUtils.canUserPerformWriteAction(report)), ), - [sortedReportActions, isOffline, report.reportID], + [sortedReportActions, isOffline, report], ); const lastAction = sortedVisibleReportActions.at(0); const sortedVisibleReportActionsObjects: OnyxTypes.ReportActions = useMemo( diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index b58de22bc520..fb1f848b677c 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -90,7 +90,8 @@ function ReportActionsView({ const route = useRoute>(); const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? -1}`, { - selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, true), + selector: (reportActions: OnyxEntry) => + ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, ReportUtils.canUserPerformWriteAction(report), true), }); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? -1}`); const prevTransactionThreadReport = usePrevious(transactionThreadReport); diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index 6fec617e4c37..290d173d9a5c 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {Ancestor} from '@libs/ReportUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -47,7 +48,12 @@ function ThreadDivider({ancestor, isLinkDisabled = false}: ThreadDividerProps) { ) : ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', ancestor.report.reportID); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + ReportUtils.canUserPerformWriteAction(ancestor.report), + ); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); if (isVisibleAction && !isOffline) { diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index 5e258436edc7..d6c573e846e8 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -89,11 +89,11 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge)); + await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, true, actionsToMerge)); }); test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => { - const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId); + const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId, true); await waitForBatchedUpdates(); await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray)); @@ -127,12 +127,12 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge)); + await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, true, actionsToMerge)); }); test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId)); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId, true)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 5f3f14971d8e..ef74f34f6413 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -306,7 +306,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, "1"); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); expect(result).toStrictEqual(expectedOutput); }); @@ -401,7 +401,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2, -1), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); expect(result).toStrictEqual(expectedOutput); }); @@ -445,7 +445,7 @@ describe('ReportActionsUtils', () => { message: [{html: '', type: 'Action type', text: 'Action text'}], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); input.pop(); expect(result).toStrictEqual(input); }); From 7b32415551e04a96e2b0d7b75378c5350bc4cfbc Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:00:04 +0530 Subject: [PATCH 046/421] fix tests. Signed-off-by: krishna2323 --- .../Attachments/AttachmentCarousel/extractAttachments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 5ebaf8af673e..620b7ce4c01d 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -99,7 +99,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action),) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } From 16ca72b963baaf7deda1e8d37a23294b13eb8a16 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:10:41 +0530 Subject: [PATCH 047/421] fix: Unchanged files with check annotations. Signed-off-by: krishna2323 --- src/libs/ReportUtils.ts | 4 ++-- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/Report.ts | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7ad1b72dfebd..7b2d592057d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8233,7 +8233,7 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry { * @param actionsToMerge * @returns containing the calculated message preview data of the report */ -function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) { +function getReportLastMessage(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge?: ReportActions) { let result: Partial = { lastMessageTranslationKey: '', lastMessageText: '', @@ -8243,7 +8243,7 @@ function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) const {lastMessageText = '', lastMessageTranslationKey = ''} = getLastVisibleMessage(reportID, actionsToMerge); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; result = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 862750920bbc..d88698e7647a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -485,7 +485,7 @@ function getOptionData({ result.alternateText = lastMessageTextFromReport.length > 0 ? ReportUtils.formatReportLastMessageText(Parser.htmlToText(lastMessageText)) - : ReportActionsUtils.getLastVisibleMessage(report.reportID, {}, lastAction)?.lastMessageText; + : ReportActionsUtils.getLastVisibleMessage(report.reportID, result.isAllowedToComment, {}, lastAction)?.lastMessageText; if (!result.alternateText) { result.alternateText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet')); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 03ad1de54b5d..20bbaf404190 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3913,9 +3913,10 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, }; - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, lastMessageText: report?.lastMessageText, @@ -3988,9 +3989,10 @@ function resolveActionableReportMentionWhisper( }, }; - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, lastMessageText: report?.lastMessageText, From 95bec1fc21766cb15edd356762c84936782092c4 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:16:54 +0530 Subject: [PATCH 048/421] minor fixes. Signed-off-by: krishna2323 --- src/libs/ReportUtils.ts | 10 ++++++---- src/libs/actions/Report.ts | 6 ++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7b2d592057d1..a466561e2d83 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2609,7 +2609,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun */ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}): LastVisibleMessage { const report = getReportOrDraftReport(reportID); - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && !isEmptyObject(report) && isChatReport(report)) { @@ -2620,7 +2620,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep } // Fetch the last visible message for report represented by reportID and based on actions to merge. - return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', actionsToMerge); + return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge); } /** @@ -8231,9 +8231,10 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry { * A function to get the report last message. This is usually used to restore the report message preview in LHN after report actions change. * @param reportID * @param actionsToMerge + * @param canUserPerformWriteActionInReport * @returns containing the calculated message preview data of the report */ -function getReportLastMessage(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge?: ReportActions) { +function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) { let result: Partial = { lastMessageTranslationKey: '', lastMessageText: '', @@ -8243,7 +8244,8 @@ function getReportLastMessage(reportID: string, canUserPerformWriteAction?: bool const {lastMessageText = '', lastMessageTranslationKey = ''} = getLastVisibleMessage(reportID, actionsToMerge); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); + const report = getReportOrDraftReport(reportID); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction(report), actionsToMerge); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; result = { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 20bbaf404190..58b4be53e2f0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3914,8 +3914,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }; const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, @@ -3990,8 +3989,7 @@ function resolveActionableReportMentionWhisper( }; const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, From 0ac89334bd30ba159603543da120676bbf158781 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:27:05 +0530 Subject: [PATCH 049/421] fix: TypeScript Checks. Signed-off-by: krishna2323 --- src/libs/actions/IOU.ts | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7ce9b9dfb272..fc16084f91fb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1734,8 +1734,12 @@ function getDeleteTrackExpenseInformation( }, ...(actionableWhisperReportActionID && {[actionableWhisperReportActionID]: {originalMessage: {resolution}}}), } as OnyxTypes.ReportActions; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '-1', updatedReportAction); - const {lastMessageText = '', lastMessageHtml = ''} = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '-1', updatedReportAction); + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); + const {lastMessageText = '', lastMessageHtml = ''} = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); // STEP 4: Build Onyx data const optimisticData: OnyxUpdate[] = []; @@ -5705,8 +5709,12 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT }, } as Record>; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1', updatedReportAction); - const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1', updatedReportAction).lastMessageText; + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); + const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction).lastMessageText; const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && (!transactionThreadID || shouldDeleteTransactionThread); @@ -5899,6 +5907,10 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo } if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } onyxUpdates.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -5906,8 +5918,12 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo value: { hasOutstandingChildRequest: false, iouReportID: null, - lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.lastMessageText, - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.created, + lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.lastMessageText, + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.created, }, }, { @@ -6015,14 +6031,21 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor } if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: { hasOutstandingChildRequest: false, iouReportID: null, - lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.lastMessageText, - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.created, + lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, {[reportPreviewAction?.reportActionID ?? '-1']: null}) + ?.lastMessageText, + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.created, }, }); optimisticData.push({ From 5fb19370ad8426a34892bf2d8a0d60b8d3b032fa Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:36:29 +0530 Subject: [PATCH 050/421] fix: TypeScript Checks. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/Task.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7778cb29d552..bd04628ea264 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1100,7 +1100,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely */ -function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction: boolean, actionsToMerge: ReportActions = {}): boolean { +function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge: ReportActions = {}): boolean { const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true)); const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c5a2442048fc..130023be885b 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -967,9 +967,10 @@ function deleteTask(report: OnyxEntry) { const optimisticReportActionID = optimisticCancelReportAction.reportActionID; const parentReportAction = getParentReportAction(report); const parentReport = getParentReport(report); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // If the task report is the last visible action in the parent report, we should navigate back to the parent report - const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1'); + const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1', canUserPerformWriteAction); const optimisticReportAction: Partial = { pendingAction: shouldDeleteTaskReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, previousMessage: parentReportAction?.message, @@ -1006,8 +1007,14 @@ function deleteTask(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, value: { - lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? '', - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions)?.created, + lastMessageText: + ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', canUserPerformWriteAction, optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? + '', + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction( + parentReport?.reportID ?? '-1', + canUserPerformWriteAction, + optimisticReportActions as OnyxTypes.ReportActions, + )?.created, hasOutstandingChildTask, }, }, From 83a70f9ab7c2d40e003be64f86118a92e1302cc5 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 1 Nov 2024 15:07:55 +0700 Subject: [PATCH 051/421] fix show QAB subtitle --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b8b76e5a00eb..c58b725a6b49 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -194,7 +194,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl }, [quickAction, translate, quickActionAvatars, quickActionReport]); const hideQABSubtitle = useMemo(() => { - if (isValidReport) { + if (!isValidReport) { return true; } if (quickActionAvatars.length === 0) { From 3ff8ba053cc4f2c732f93617e7f26d2f462b955e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:13:20 +0530 Subject: [PATCH 052/421] feat: animation after approving an expense. Signed-off-by: krishna2323 --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +++ .../ReportActionItem/ReportPreview.tsx | 23 +++++++++++++--- .../AnimatedSettlementButton.tsx | 27 +++++++++++++++---- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 3d6ad9006dc5..ba320a594135 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -66,6 +66,9 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { + if (startAnimation) { + startAnimation(); + } IOU.approveMoneyRequest(moneyRequestReport, full); if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? '')); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index dc4e396ee75e..c2be6db0a4aa 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -120,6 +120,7 @@ function ReportPreview({ ); const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false); + const [isApprovedAnimationRunning, setIsApprovedAnimationRunning] = useState(false); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); @@ -200,11 +201,18 @@ function ReportPreview({ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []); + const stopAnimation = useCallback(() => { + setIsPaidAnimationRunning(false); + setIsApprovedAnimationRunning(false); + }, []); const startAnimation = useCallback(() => { setIsPaidAnimationRunning(true); HapticFeedback.longPress(); }, []); + const startApprovedAnimation = useCallback(() => { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); + }, []); const confirmPayment = useCallback( (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { @@ -236,6 +244,8 @@ function ReportPreview({ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); IOU.approveMoneyRequest(iouReport, true); } }; @@ -427,7 +437,7 @@ function ReportPreview({ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isPaidAnimationRunning || isApprovedAnimationRunning) { return; } @@ -556,6 +566,7 @@ function ReportPreview({ { + if (requestType === 'approve') { + startApprovedAnimation(); + } else { + startAnimation(); + } + }} /> )} diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 5de528d741a2..375e76a33582 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -11,9 +11,10 @@ import type SettlementButtonProps from './types'; type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; + isApprovedAnimationRunning: boolean; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -38,7 +39,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isPaidAnimationRunning + const buttonDisabledStyle = isApprovedAnimationRunning ? { opacity: 1, ...styles.cursorDefault, @@ -56,7 +57,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isApprovedAnimationRunning && !isPaidAnimationRunning) { resetAnimation(); return; } @@ -73,7 +74,18 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); - }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]); + }, [ + isPaidAnimationRunning, + isApprovedAnimationRunning, + onAnimationFinish, + buttonOpacity, + buttonScale, + height, + paymentCompleteTextOpacity, + paymentCompleteTextScale, + buttonMarginTop, + resetAnimation, + ]); return ( @@ -82,11 +94,16 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is {translate('iou.paymentComplete')} )} + {isApprovedAnimationRunning && ( + + {translate('iou.approved')} + + )} From 31ddb55edabc6b07126cd908af2631fc63633e8e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:38:27 +0530 Subject: [PATCH 053/421] minor fixes. Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 6 +++- .../AnimatedSettlementButton.tsx | 34 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c2be6db0a4aa..27068ff2f80f 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -203,6 +203,9 @@ function ReportPreview({ const stopAnimation = useCallback(() => { setIsPaidAnimationRunning(false); + }, []); + + const stopApprovedAnimation = useCallback(() => { setIsApprovedAnimationRunning(false); }, []); const startAnimation = useCallback(() => { @@ -567,6 +570,7 @@ function ReportPreview({ onlyShowPayElsewhere={onlyShowPayElsewhere} isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} + onApprovedAnimationFinish={stopApprovedAnimation} onAnimationFinish={stopAnimation} formattedAmount={getSettlementAmount() ?? ''} currency={iouReport?.currency} @@ -636,7 +640,7 @@ function ReportPreview({ moneyRequestReport={iouReport} transactionCount={numberOfRequests} startAnimation={() => { - if (requestType === 'approve') { + if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) { startApprovedAnimation(); } else { startAnimation(); diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 375e76a33582..f8205a1b1ab0 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -12,9 +12,17 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; isApprovedAnimationRunning: boolean; + onApprovedAnimationFinish: () => void; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({ + isPaidAnimationRunning, + onAnimationFinish, + isApprovedAnimationRunning, + onApprovedAnimationFinish, + isDisabled, + ...settlementButtonProps +}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -39,12 +47,13 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isApprovedAnimationRunning - ? { - opacity: 1, - ...styles.cursorDefault, - } - : undefined; + const buttonDisabledStyle = + isPaidAnimationRunning || isApprovedAnimationRunning + ? { + opacity: 1, + ...styles.cursorDefault, + } + : undefined; const resetAnimation = useCallback(() => { // eslint-disable-next-line react-compiler/react-compiler @@ -70,7 +79,15 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => + runOnJS(() => { + if (isApprovedAnimationRunning) { + onApprovedAnimationFinish(); + } else { + onAnimationFinish(); + } + })(), + ), ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); @@ -85,6 +102,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is paymentCompleteTextScale, buttonMarginTop, resetAnimation, + onApprovedAnimationFinish, ]); return ( From a8154c46aaadca4f78d3bdf1747177ef4927eea9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 14:31:12 +0530 Subject: [PATCH 054/421] feat: Update Default / Custom Workspace Invite Behavior. Signed-off-by: krishna2323 --- .../workspace/WorkspaceInviteMessagePage.tsx | 16 ++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 89cab963fb43..f0317284e8f9 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -20,6 +19,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; +import * as FormActions from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; @@ -48,6 +48,7 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [formData] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT); const viewportOffsetTop = useViewportOffsetTop(); const [welcomeNote, setWelcomeNote] = useState(); @@ -66,6 +67,8 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] || // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing workspaceInviteMessageDraft || @@ -76,7 +79,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: workspaceName: policy?.name ?? '', }) ); - }, [workspaceInviteMessageDraft, policy, translate]); + }, [workspaceInviteMessageDraft, policy, translate, formData]); useEffect(() => { if (isOnyxLoading) { @@ -93,16 +96,13 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOnyxLoading]); - const debouncedSaveDraft = lodashDebounce((newDraft: string | null) => { - Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); - }); - const sendInvitation = () => { Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs); - debouncedSaveDraft(null); + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); Navigation.dismissModal(); }; @@ -194,7 +194,6 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: value={welcomeNote} onChangeText={(text: string) => { setWelcomeNote(text); - debouncedSaveDraft(text); }} ref={(element: AnimatedTextInputRef) => { if (!element) { @@ -205,6 +204,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: } inputCallbackRef(element); }} + shouldSaveDraft /> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index ad48d15aa9df..bfa13ef3f65d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -16,6 +16,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as FormActions from '@libs/actions/FormActions'; import * as ReportActions from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -78,6 +79,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli useEffect(() => { return () => { Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [route.params.policyID]); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 96b6d31e5a2e..cb914591a59d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -30,6 +30,7 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as FormActions from '@libs/actions/FormActions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -417,6 +418,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails, 1500); Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }, [invitedEmailsToAccountIDsDraft, route.params.policyID, isFocused, accountIDs, prevAccountIDs]); const getHeaderMessage = () => { From 936b5bd45d821e34eecbf852f9627c1cef0f7d45 Mon Sep 17 00:00:00 2001 From: jaydamani Date: Sat, 2 Nov 2024 13:23:30 +0100 Subject: [PATCH 055/421] feature(onboarding): combine category and tag setup task for connections --- src/CONST.ts | 9 +++++++++ src/libs/actions/Report.ts | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 437ee4e7fd42..b79c831264eb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4891,6 +4891,15 @@ const CONST = { '\n' + `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, }, + { + type: 'addAccountingIntegration', + autoCompleted: false, + title: 'Set up categories and tags', + description: ({workspaceCategoriesLink, workspaceAccountingLink}) => + '*Set up categories and tags* so your team can code expenses for easy reporting.\n' + + '\n' + + `Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceCategoriesLink}).`, + }, { type: 'setupCategories', autoCompleted: false, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3eac21cd1b18..0333577fff8d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3439,7 +3439,11 @@ function completeOnboarding( const tasksData = data.tasks .filter((task) => { - if (task.type === 'addAccountingIntegration' && !userReportedIntegration) { + if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) { + return false; + } + + if (['addAccountingIntegration', 'setupCategoriesAndTags'].includes(task.type) && !userReportedIntegration) { return false; } return true; From 3c8de6dbc3ff0c764b6d4973d664ea7c386c0f16 Mon Sep 17 00:00:00 2001 From: jaydamani Date: Sat, 2 Nov 2024 13:32:05 +0100 Subject: [PATCH 056/421] update task type --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index b79c831264eb..1ce3b14acc53 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4892,7 +4892,7 @@ const CONST = { `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, }, { - type: 'addAccountingIntegration', + type: 'setupCategoriesAndTags', autoCompleted: false, title: 'Set up categories and tags', description: ({workspaceCategoriesLink, workspaceAccountingLink}) => From c744ec566bd88219433decba6f74db3d00e62965 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:33:34 +0700 Subject: [PATCH 057/421] fix image from opening in both a new tab and app modal --- src/ROUTES.ts | 6 +++-- .../BaseAnchorForCommentsOnly.tsx | 6 ++--- src/components/AnchorForCommentsOnly/types.ts | 2 ++ src/components/AttachmentModal.tsx | 22 ++++++++++++++++++ .../AttachmentCarousel/extractAttachments.ts | 13 +++++++++++ src/components/Attachments/types.ts | 2 ++ .../HTMLRenderers/AnchorRenderer.tsx | 2 ++ .../HTMLRenderers/ImageRenderer.tsx | 3 ++- src/components/Header.tsx | 23 +++++++++++++++++-- src/components/HeaderWithBackButton/index.tsx | 3 +++ src/components/HeaderWithBackButton/types.ts | 2 ++ src/libs/Navigation/types.ts | 1 + src/pages/home/report/ReportAttachments.tsx | 6 +++-- 13 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 45501bf46374..fbdfbd937717 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,11 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageHrefLink?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}` as const; + const imageHrefLinkParam = imageHrefLink ? `&imageHrefLink=${imageHrefLink}` : ''; + + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageHrefLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 4c470858292c..3b1427f71e8b 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -17,7 +17,7 @@ import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; /* * This is a default anchor component for regular links. */ -function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, ...rest}: BaseAnchorForCommentsOnlyProps) { +function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, containsImageLink, ...rest}: BaseAnchorForCommentsOnlyProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const linkRef = useRef(null); @@ -62,7 +62,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', role={CONST.ROLE.LINK} accessibilityLabel={href} > - + void; + + containsImageLink?: boolean; }; type BaseAnchorForCommentsOnlyProps = AnchorForCommentsOnlyProps & { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 0bc233812ca7..ebd5e6ef8c9c 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -134,6 +134,8 @@ type AttachmentModalProps = { canEditReceipt?: boolean; shouldDisableSendButton?: boolean; + + imageHrefLink?: string; }; function AttachmentModal({ @@ -161,6 +163,7 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, + imageHrefLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -185,6 +188,7 @@ function AttachmentModal({ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const [attachmentCarouselImageHref, setAttachmentCarouselImageHref] = useState(''); const [file, setFile] = useState( originalFileName @@ -211,6 +215,7 @@ function AttachmentModal({ setFile(attachment.file); setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); onCarouselAttachmentChange(attachment); + setAttachmentCarouselImageHref(attachment?.imageHrefLink ?? ''); }, [onCarouselAttachmentChange], ); @@ -482,6 +487,22 @@ function AttachmentModal({ const submitRef = useRef(null); + const getImageHrefLink = () => { + if (shouldShowNotFoundPage) { + return ''; + } + + if (!isEmptyObject(report) && !isReceiptAttachment) { + return attachmentCarouselImageHref; + } + + if (!isAuthTokenRequired && imageHrefLink) { + return imageHrefLink; + } + + return ''; + }; + return ( <> {isLoading && } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 81ee6d08934b..30042df76a54 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -28,8 +28,13 @@ function extractAttachments( // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. const uniqueSources = new Set(); + let currentLinkHref = ''; + const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { + if (name === 'a' && attribs.href) { + currentLinkHref = attribs.href; + } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); if (uniqueSources.has(source)) { @@ -81,9 +86,17 @@ function extractAttachments( file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', + imageHrefLink: currentLinkHref, }); } }, + onclosetag: (name) => { + if (!(name === 'a') || !currentLinkHref) { + return; + } + + currentLinkHref = ''; + }, }); if (type === CONST.ATTACHMENT_TYPE.NOTE) { diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index 8bac4cc53af6..e148389ff244 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -28,6 +28,8 @@ type Attachment = { isReceipt?: boolean; duration?: number; + + imageHrefLink?: string; }; export type {AttachmentSource, Attachment}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 122db1e7877b..6cb23b0dd045 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -30,6 +30,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); + const containsImageLink = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; @@ -73,6 +74,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} + containsImageLink={containsImageLink} > { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 86a7a9cabcb6..bd3f32abfd1e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,10 +1,11 @@ import type {ReactNode} from 'react'; import React, {useMemo} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {Linking, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EnvironmentBadge from './EnvironmentBadge'; import Text from './Text'; +import TextLink from './TextLink'; type HeaderProps = { /** Title of the Header */ @@ -21,9 +22,11 @@ type HeaderProps = { /** Additional header container styles */ containerStyles?: StyleProp; + + imageHrefLink?: string; }; -function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, imageHrefLink = ''}: HeaderProps) { const styles = useThemeStyles(); const renderedSubtitle = useMemo( () => ( @@ -43,6 +46,21 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ), [subtitle, styles], ); + + const renderedImageHrefLink = () => { + return ( + + { + Linking.openURL(imageHrefLink); + }} + > + {imageHrefLink} + + + ); + }; + return ( @@ -57,6 +75,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} + {imageHrefLink && renderedImageHrefLink()} {shouldShowEnvironmentBadge && } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 0d307aa8728d..8e12f47af106 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -65,6 +65,7 @@ function HeaderWithBackButton({ shouldDisplaySearchRouter = false, progressBarPercentage, style, + imageHrefLink = '', }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -108,10 +109,12 @@ function HeaderWithBackButton({ title={title} subtitle={stepCounter ? translate('stepCounter', stepCounter) : subtitle} textStyles={[titleColor ? StyleUtils.getTextColorStyle(titleColor) : {}, isCentralPaneSettings && styles.textHeadlineH2]} + imageHrefLink={imageHrefLink} /> ); }, [ StyleUtils, + imageHrefLink, isCentralPaneSettings, policy, progressBarPercentage, diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 12dc1aa9684b..c9fa0ba4d7b5 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -142,6 +142,8 @@ type HeaderWithBackButtonProps = Partial & { /** Additional styles to add to the component */ style?: StyleProp; + + imageHrefLink?: string; }; export type {ThreeDotsMenuItem}; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3eae46ac2855..4b231668ef70 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1549,6 +1549,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & type: ValueOf; accountID: string; isAuthTokenRequired?: string; + imageHrefLink?: string; }; [SCREENS.PROFILE_AVATAR]: { accountID: string; diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index d30d8e9aabc1..fe95cab404ce 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -18,6 +18,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const type = route.params.type; const accountID = route.params.accountID; const isAuthTokenRequired = route.params.isAuthTokenRequired; + const imageHrefLink = route.params.imageHrefLink; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); @@ -26,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID)); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageHrefLink); Navigation.navigate(routeToNavigate); }, - [reportID, accountID, type], + [reportID, type, accountID, isAuthTokenRequired, imageHrefLink], ); return ( @@ -48,6 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { onCarouselAttachmentChange={onCarouselAttachmentChange} shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} isAuthTokenRequired={!!isAuthTokenRequired} + imageHrefLink={imageHrefLink ?? ''} /> ); } From d5490b83d4059c4b940f874cd76578ac92f97f6f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:01:11 +0700 Subject: [PATCH 058/421] update isEnd to false when the video is playing --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index fad641e696ae..4a18f9abeb69 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,6 +188,8 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); + console.log(isEnded); + const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); @@ -207,6 +209,8 @@ function BaseVideoPlayer({ setIsEnded(status.didJustFinish && !status.isLooping); setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; + } else if (status.isPlaying && isEnded) { + setIsEnded(false); } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { From 4acf40ca91ed28432d3a0e56cbd8812d22367140 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:02:05 +0700 Subject: [PATCH 059/421] remove log --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 4a18f9abeb69..1e46c595610e 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,8 +188,6 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); - console.log(isEnded); - const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); From df706737f05689a4ca64c5e537790c3caaa17439 Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 5 Nov 2024 11:08:12 +0100 Subject: [PATCH 060/421] fix window dimensions --- src/hooks/useWindowDimensions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index 4997fc4b01a7..a1bea6a80644 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -130,7 +130,7 @@ export default function (useCachedViewportHeight = false): WindowDimensions { const didScreenReturnToOriginalSize = lockedWindowDimensionsRef.current.windowWidth === windowWidth && lockedWindowDimensionsRef.current.windowHeight === windowHeight; // if video exits fullscreen mode, unlock the window dimensions - if (lockedWindowDimensionsRef.current && !isFullScreenRef.current && didScreenReturnToOriginalSize) { + if (lockedWindowDimensionsRef.current && !isFullScreenRef.current) { const lastLockedWindowDimensions = {...lockedWindowDimensionsRef.current}; unlockWindowDimensions(); return {windowWidth: lastLockedWindowDimensions.windowWidth, windowHeight: lastLockedWindowDimensions.windowHeight}; From 9e37309782b252f70c289e98090b13d5afe1422e Mon Sep 17 00:00:00 2001 From: Etotaziba Olei Date: Tue, 5 Nov 2024 11:22:25 +0100 Subject: [PATCH 061/421] fix lint --- src/hooks/useWindowDimensions/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hooks/useWindowDimensions/index.ts b/src/hooks/useWindowDimensions/index.ts index a1bea6a80644..6e7b74bf8df5 100644 --- a/src/hooks/useWindowDimensions/index.ts +++ b/src/hooks/useWindowDimensions/index.ts @@ -127,8 +127,6 @@ export default function (useCachedViewportHeight = false): WindowDimensions { return windowDimensions; } - const didScreenReturnToOriginalSize = lockedWindowDimensionsRef.current.windowWidth === windowWidth && lockedWindowDimensionsRef.current.windowHeight === windowHeight; - // if video exits fullscreen mode, unlock the window dimensions if (lockedWindowDimensionsRef.current && !isFullScreenRef.current) { const lastLockedWindowDimensions = {...lockedWindowDimensionsRef.current}; From 7e6f6c14e0d5d20969e35874b4790df7c5b0c725 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 5 Nov 2024 20:04:21 +0700 Subject: [PATCH 062/421] fix: update trans --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index da158b15cd77..a2d230b141ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,7 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', - allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, + allowLocationFromSetting: `El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que estés. Por favor, permite el acceso a la ubicación en la configuración de permisos de tu dispositivo.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 5dbc039f0deec5e66f35432dad8984452bef7347 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:22:09 +0700 Subject: [PATCH 063/421] rename variables and functions for clarity --- src/ROUTES.ts | 6 +++--- .../BaseAnchorForCommentsOnly.tsx | 6 +++--- src/components/AnchorForCommentsOnly/types.ts | 3 ++- src/components/AttachmentModal.tsx | 18 +++++++++--------- .../AttachmentCarousel/extractAttachments.ts | 10 +++++----- src/components/Attachments/types.ts | 2 +- .../HTMLRenderers/AnchorRenderer.tsx | 4 ++-- .../HTMLRenderers/ImageRenderer.tsx | 4 ++-- src/components/Header.tsx | 13 +++++++------ src/components/HeaderWithBackButton/index.tsx | 6 +++--- src/components/HeaderWithBackButton/types.ts | 3 ++- src/libs/Navigation/types.ts | 2 +- src/pages/home/report/ReportAttachments.tsx | 8 ++++---- 13 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fbdfbd937717..0cff24fadf8a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -310,13 +310,13 @@ const ROUTES = { }, ATTACHMENTS: { route: 'attachment', - getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageHrefLink?: string) => { + getRoute: (reportID: string, type: ValueOf, url: string, accountID?: number, isAuthTokenRequired?: boolean, imageLink?: string) => { const reportParam = reportID ? `&reportID=${reportID}` : ''; const accountParam = accountID ? `&accountID=${accountID}` : ''; const authTokenParam = isAuthTokenRequired ? '&isAuthTokenRequired=true' : ''; - const imageHrefLinkParam = imageHrefLink ? `&imageHrefLink=${imageHrefLink}` : ''; + const imageLinkParam = imageLink ? `&imageLink=${imageLink}` : ''; - return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageHrefLinkParam}` as const; + return `attachment?source=${encodeURIComponent(url)}&type=${type}${reportParam}${accountParam}${authTokenParam}${imageLinkParam}` as const; }, }, REPORT_PARTICIPANTS: { diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx index 3b1427f71e8b..d31dcb5df7b6 100644 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx +++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx @@ -17,7 +17,7 @@ import type {BaseAnchorForCommentsOnlyProps, LinkProps} from './types'; /* * This is a default anchor component for regular links. */ -function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, containsImageLink, ...rest}: BaseAnchorForCommentsOnlyProps) { +function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', target = '', children = null, style, onPress, isLinkHasImage, ...rest}: BaseAnchorForCommentsOnlyProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const linkRef = useRef(null); @@ -62,7 +62,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '', role={CONST.ROLE.LINK} accessibilityLabel={href} > - + void; - containsImageLink?: boolean; + /** Indicates whether an image is wrapped in an anchor (``) tag with an `href` link */ + isLinkHasImage?: boolean; }; type BaseAnchorForCommentsOnlyProps = AnchorForCommentsOnlyProps & { diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index ebd5e6ef8c9c..a343cd556149 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -135,7 +135,7 @@ type AttachmentModalProps = { shouldDisableSendButton?: boolean; - imageHrefLink?: string; + imageLink?: string; }; function AttachmentModal({ @@ -163,7 +163,7 @@ function AttachmentModal({ type = undefined, accountID = undefined, shouldDisableSendButton = false, - imageHrefLink = '', + imageLink = '', }: AttachmentModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -188,7 +188,7 @@ function AttachmentModal({ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const [attachmentCarouselImageHref, setAttachmentCarouselImageHref] = useState(''); + const [attachmentCarouselImageLink, setAttachmentCarouselImageLink] = useState(''); const [file, setFile] = useState( originalFileName @@ -215,7 +215,7 @@ function AttachmentModal({ setFile(attachment.file); setIsAuthTokenRequiredState(attachment.isAuthTokenRequired ?? false); onCarouselAttachmentChange(attachment); - setAttachmentCarouselImageHref(attachment?.imageHrefLink ?? ''); + setAttachmentCarouselImageLink(attachment?.imageLink ?? ''); }, [onCarouselAttachmentChange], ); @@ -487,17 +487,17 @@ function AttachmentModal({ const submitRef = useRef(null); - const getImageHrefLink = () => { + const getSubTitleLink = () => { if (shouldShowNotFoundPage) { return ''; } if (!isEmptyObject(report) && !isReceiptAttachment) { - return attachmentCarouselImageHref; + return attachmentCarouselImageLink; } - if (!isAuthTokenRequired && imageHrefLink) { - return imageHrefLink; + if (!isAuthTokenRequired && imageLink) { + return imageLink; } return ''; @@ -548,7 +548,7 @@ function AttachmentModal({ threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} threeDotsMenuItems={threeDotsMenuItems} shouldOverlayDots - imageHrefLink={getImageHrefLink()} + subTitleLink={getSubTitleLink()} /> {isLoading && } diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 30042df76a54..251bbf64e5cd 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -28,12 +28,12 @@ function extractAttachments( // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. const uniqueSources = new Set(); - let currentLinkHref = ''; + let currentImageLink = ''; const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { if (name === 'a' && attribs.href) { - currentLinkHref = attribs.href; + currentImageLink = attribs.href; } if (name === 'video') { const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]); @@ -86,16 +86,16 @@ function extractAttachments( file: {name: fileName, width, height}, isReceipt: false, hasBeenFlagged: attribs['data-flagged'] === 'true', - imageHrefLink: currentLinkHref, + imageLink: currentImageLink, }); } }, onclosetag: (name) => { - if (!(name === 'a') || !currentLinkHref) { + if (!(name === 'a') || !currentImageLink) { return; } - currentLinkHref = ''; + currentImageLink = ''; }, }); diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts index e148389ff244..a819e3ee9075 100644 --- a/src/components/Attachments/types.ts +++ b/src/components/Attachments/types.ts @@ -29,7 +29,7 @@ type Attachment = { duration?: number; - imageHrefLink?: string; + imageLink?: string; }; export type {AttachmentSource, Attachment}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx index 6cb23b0dd045..1af172d07eea 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx @@ -30,7 +30,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref); const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref); const isVideo = attrHref && Str.isVideo(attrHref); - const containsImageLink = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); + const isLinkHasImage = tnode.tagName === 'a' && tnode.children.some((child) => child.tagName === 'img'); const isDeleted = HTMLEngineUtils.isDeletedNode(tnode); const textDecorationLineStyle = isDeleted ? styles.underlineLineThrough : {}; @@ -74,7 +74,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) { key={key} // Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined} - containsImageLink={containsImageLink} + isLinkHasImage={isLinkHasImage} > { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bd3f32abfd1e..c0e020120511 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -23,10 +23,11 @@ type HeaderProps = { /** Additional header container styles */ containerStyles?: StyleProp; - imageHrefLink?: string; + /** The URL link associated with the attachment's subtitle, if available */ + subTitleLink?: string; }; -function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, imageHrefLink = ''}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false, subTitleLink = ''}: HeaderProps) { const styles = useThemeStyles(); const renderedSubtitle = useMemo( () => ( @@ -47,15 +48,15 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ [subtitle, styles], ); - const renderedImageHrefLink = () => { + const renderedSubTitleLink = () => { return ( { - Linking.openURL(imageHrefLink); + Linking.openURL(subTitleLink); }} > - {imageHrefLink} + {subTitleLink} ); @@ -75,7 +76,7 @@ function Header({title = '', subtitle = '', textStyles = [], containerStyles = [ ) : title} {renderedSubtitle} - {imageHrefLink && renderedImageHrefLink()} + {subTitleLink && renderedSubTitleLink()} {shouldShowEnvironmentBadge && } diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 8e12f47af106..2c07c48d52b7 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -65,7 +65,7 @@ function HeaderWithBackButton({ shouldDisplaySearchRouter = false, progressBarPercentage, style, - imageHrefLink = '', + subTitleLink = '', }: HeaderWithBackButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -109,12 +109,12 @@ function HeaderWithBackButton({ title={title} subtitle={stepCounter ? translate('stepCounter', stepCounter) : subtitle} textStyles={[titleColor ? StyleUtils.getTextColorStyle(titleColor) : {}, isCentralPaneSettings && styles.textHeadlineH2]} - imageHrefLink={imageHrefLink} + subTitleLink={subTitleLink} /> ); }, [ StyleUtils, - imageHrefLink, + subTitleLink, isCentralPaneSettings, policy, progressBarPercentage, diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index c9fa0ba4d7b5..6eef2b072eee 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -143,7 +143,8 @@ type HeaderWithBackButtonProps = Partial & { /** Additional styles to add to the component */ style?: StyleProp; - imageHrefLink?: string; + /** The URL link associated with the attachment's subtitle, if available */ + subTitleLink?: string; }; export type {ThreeDotsMenuItem}; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4b231668ef70..0ef81d10dede 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1549,7 +1549,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & type: ValueOf; accountID: string; isAuthTokenRequired?: string; - imageHrefLink?: string; + imageLink?: string; }; [SCREENS.PROFILE_AVATAR]: { accountID: string; diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index fe95cab404ce..c002d109504f 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -18,7 +18,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const type = route.params.type; const accountID = route.params.accountID; const isAuthTokenRequired = route.params.isAuthTokenRequired; - const imageHrefLink = route.params.imageHrefLink; + const imageLink = route.params.imageLink; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID || -1}`); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); @@ -27,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageHrefLink); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageLink); Navigation.navigate(routeToNavigate); }, - [reportID, type, accountID, isAuthTokenRequired, imageHrefLink], + [reportID, type, accountID, isAuthTokenRequired, imageLink], ); return ( @@ -49,7 +49,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { onCarouselAttachmentChange={onCarouselAttachmentChange} shouldShowNotFoundPage={!isLoadingApp && type !== CONST.ATTACHMENT_TYPE.SEARCH && !report?.reportID} isAuthTokenRequired={!!isAuthTokenRequired} - imageHrefLink={imageHrefLink ?? ''} + imageLink={imageLink ?? ''} /> ); } From dbc52033d86ffc50a7ce8d917a57dd535b9cc56f Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:27:54 +0700 Subject: [PATCH 064/421] update parameters for route in onCarouselAttachmentChange function --- src/pages/home/report/ReportAttachments.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index c002d109504f..372c5fd1f5ec 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -27,10 +27,10 @@ function ReportAttachments({route}: ReportAttachmentsProps) { const onCarouselAttachmentChange = useCallback( (attachment: Attachment) => { - const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), !!isAuthTokenRequired, imageLink); + const routeToNavigate = ROUTES.ATTACHMENTS.getRoute(reportID, type, String(attachment.source), Number(accountID), attachment.isAuthTokenRequired, attachment.imageLink); Navigation.navigate(routeToNavigate); }, - [reportID, type, accountID, isAuthTokenRequired, imageLink], + [reportID, type, accountID], ); return ( From 766e05bca88f1fa039404b0e0d6b92cce7452560 Mon Sep 17 00:00:00 2001 From: jaydamani Date: Tue, 5 Nov 2024 19:55:41 +0100 Subject: [PATCH 065/421] update link in setupCategoriesAndTags --- src/CONST.ts | 5 +++-- src/libs/actions/Report.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 1ce3b14acc53..97b8ac45acc7 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -264,6 +264,7 @@ type OnboardingTaskType = { workspaceMembersLink: string; integrationName: string; workspaceAccountingLink: string; + workspaceSettingsLink: string; }>, ) => string); }; @@ -4895,10 +4896,10 @@ const CONST = { type: 'setupCategoriesAndTags', autoCompleted: false, title: 'Set up categories and tags', - description: ({workspaceCategoriesLink, workspaceAccountingLink}) => + description: ({workspaceSettingsLink, workspaceAccountingLink}) => '*Set up categories and tags* so your team can code expenses for easy reporting.\n' + '\n' + - `Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceCategoriesLink}).`, + `Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceSettingsLink}).`, }, { type: 'setupCategories', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 0333577fff8d..b560dc244d89 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3458,6 +3458,7 @@ function completeOnboarding( workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID ?? '-1')}`, integrationName, workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID ?? '-1')}`, + workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID ?? '01')}`, }) : task.description; const taskTitle = From 9070620537f3ca0bd022dee4c8f495a904b7f469 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 02:36:29 +0530 Subject: [PATCH 066/421] minor update. Signed-off-by: krishna2323 --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index f0317284e8f9..4b437e1ffd78 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -68,16 +68,16 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formData?.[INPUT_IDS.WELCOME_MESSAGE] || + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - workspaceInviteMessageDraft || + workspaceInviteMessageDraft ?? // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - Parser.htmlToMarkdown(policy?.description ?? '') || - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }) + (Parser.htmlToMarkdown(policy?.description ?? '') || + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + })) ); }, [workspaceInviteMessageDraft, policy, translate, formData]); From c57030110d6b8c9ba0a9a453352c4b417f856935 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 6 Nov 2024 09:00:19 +0700 Subject: [PATCH 067/421] Fix list not scrolled up when search query empty --- src/components/Search/SearchRouter/SearchRouter.tsx | 7 ++++++- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 83d7d5d89b20..6f62a08db00f 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -37,6 +37,7 @@ import ROUTES from '@src/ROUTES'; import SearchRouterInput from './SearchRouterInput'; import SearchRouterList from './SearchRouterList'; import type {ItemWithQuery} from './SearchRouterList'; +import isEmpty from 'lodash/isEmpty'; type SearchRouterProps = { onRouterClose: () => void; @@ -293,6 +294,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ], ); + const prevUserQueryRef = useRef(null); const onSearchChange = useCallback( (userQuery: string) => { let newUserQuery = userQuery; @@ -302,11 +304,14 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { setTextInputValue(newUserQuery); const autocompleteParsedQuery = parseForAutocomplete(newUserQuery); updateAutocomplete(autocompleteParsedQuery?.autocomplete?.value ?? '', autocompleteParsedQuery?.ranges ?? [], autocompleteParsedQuery?.autocomplete?.key); - if (newUserQuery) { + if (newUserQuery || !isEmpty(prevUserQueryRef.current)) { listRef.current?.updateAndScrollToFocusedIndex(0); } else { listRef.current?.updateAndScrollToFocusedIndex(-1); } + + // Store the previous newUserQuery + prevUserQueryRef.current = newUserQuery; }, [autocompleteSuggestions, setTextInputValue, updateAutocomplete], ); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 3e1b3a3c2d70..ffb6c64a0fc5 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -572,8 +572,9 @@ function BaseSelectionList( } // Remove the focus if the search input is empty or selected options length is changed (and allOptions length remains the same) // else focus on the first non disabled item + const newSelectedIndex = - textInputValue === '' || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; + (isEmpty(prevTextInputValue) && textInputValue === '') || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; // reseting the currrent page to 1 when the user types something setCurrentPage(1); From 597791ac2162aa03430c6cb895dc414a5aa53a6a Mon Sep 17 00:00:00 2001 From: burczu Date: Tue, 29 Oct 2024 13:18:10 +0100 Subject: [PATCH 068/421] enter email, hang tight and director check components added --- .../simple-illustration__pillow.svg | 28 +++++ src/components/Icon/Illustrations.ts | 2 + src/languages/en.ts | 21 ++++ src/languages/es.ts | 23 +++- src/languages/params.ts | 5 + .../NonUSD/SignerInfo/DirectorCheck.tsx | 65 ++++++++++ .../NonUSD/SignerInfo/EnterEmail.tsx | 83 +++++++++++++ .../NonUSD/SignerInfo/HangTight.tsx | 58 +++++++++ src/types/form/ReimbursementAccountForm.ts | 117 ++++++++++++++++++ 9 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__pillow.svg create mode 100644 src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx diff --git a/assets/images/simple-illustrations/simple-illustration__pillow.svg b/assets/images/simple-illustrations/simple-illustration__pillow.svg new file mode 100644 index 000000000000..97a0811266ae --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__pillow.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 0efb65ed7a61..98142ec02def 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -102,6 +102,7 @@ import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__o import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg'; import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__piggybank.svg'; +import Pillow from '@assets/images/simple-illustrations/simple-illustration__pillow.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; @@ -222,6 +223,7 @@ export { ExpensifyCardIllustration, SplitBill, PiggyBank, + Pillow, Accounting, Car, Coins, diff --git a/src/languages/en.ts b/src/languages/en.ts index f1339ed88373..0ac1767687d7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -41,6 +41,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -2257,6 +2258,26 @@ const translations = { }, signerInfoStep: { signerInfo: 'Signer info', + areYouDirector: ({companyName}: CompanyNameParams) => `Are you a director or senior officer at ${companyName}?`, + regulationRequiresUs: 'Regulation requires us to verify if the signer has the authority to take this action on behalf of the business.', + whatsYourName: "What's your legal name", + fullName: 'Legal full name', + whatsYourJobTitle: "What's your job title?", + jobTitle: 'Job title', + whatsYourDOB: "What's your date of birth?", + uploadID: 'Upload ID and proof of address', + id: "ID (driver's license or passport)", + personalAddress: 'Proof of personal address (e.g. utility bill)', + letsDoubleCheck: 'Let’s double check that everything looks right.', + legalName: 'Legal name', + proofOf: 'Proof of personal address', + enterOneEmail: 'Enter the email of director or senior officer at', + regulationRequiresOneMoreDirector: 'Regulation requires one more director or senior officer as a signer.', + hangTight: 'Hang tight...', + enterTwoEmails: 'Enter the emails of two directors or senior officers at', + sendReminder: 'Send a reminder', + chooseFile: 'Choose file', + weAreWaiting: "We're waiting for others to verify their identities as directors or senior officers of the business.", }, agreementsStep: { agreements: 'Agreements', diff --git a/src/languages/es.ts b/src/languages/es.ts index a9ebfedf1cc3..12e1de851c67 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -39,6 +39,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -2279,7 +2280,27 @@ const translations = { selectCountry: 'Seleccione su país', }, signerInfoStep: { - signerInfo: 'Información del firmante', + signerInfo: 'Signer info', + areYouDirector: ({companyName}: CompanyNameParams) => `Are you a director or senior officer at ${companyName}?`, + regulationRequiresUs: 'Regulation requires us to verify if the signer has the authority to take this action on behalf of the business.', + whatsYourName: "What's your legal name", + fullName: 'Legal full name', + whatsYourJobTitle: "What's your job title?", + jobTitle: 'Job title', + whatsYourDOB: "What's your date of birth?", + uploadID: 'Upload ID and proof of address', + id: "ID (driver's license or passport)", + personalAddress: 'Proof of personal address (e.g. utility bill)', + letsDoubleCheck: 'Let’s double check that everything looks right.', + legalName: 'Legal name', + proofOf: 'Proof of personal address', + enterOneEmail: 'Enter the email of director or senior officer at', + regulationRequiresOneMoreDirector: 'Regulation requires one more director or senior officer as a signer.', + hangTight: 'Hang tight...', + enterTwoEmails: 'Enter the emails of two directors or senior officers at', + sendReminder: 'Send a reminder', + chooseFile: 'Choose file', + weAreWaiting: "We're waiting for others to verify their identities as directors or senior officers of the business.", }, agreementsStep: { agreements: 'Acuerdos', diff --git a/src/languages/params.ts b/src/languages/params.ts index e9f0c4370357..79eae25ae9a6 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -547,6 +547,10 @@ type CompanyCardBankName = { bankName: string; }; +type CompanyNameParams = { + companyName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -746,4 +750,5 @@ export type { OptionalParam, AssignCardParams, ImportedTypesParams, + CompanyNameParams, }; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx new file mode 100644 index 000000000000..648fd37e543b --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx @@ -0,0 +1,65 @@ +import React, {useMemo, useState} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import type {Choice} from '@components/RadioButtons'; +import RadioButtons from '@components/RadioButtons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type DirectorCheckProps = { + /** The title of the question */ + title: string; + + /** The default value of the radio button */ + defaultValue: boolean; + + /** Callback when the value is selected */ + onSelectedValue: (value: boolean) => void; +}; + +function DirectorCheck({title, onSelectedValue, defaultValue}: DirectorCheckProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [value, setValue] = useState(defaultValue); + + const handleSubmit = () => { + onSelectedValue(value); + }; + const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); + const options = useMemo( + () => [ + { + label: translate('common.yes'), + value: 'true', + }, + { + label: translate('common.no'), + value: 'false', + }, + ], + [translate], + ); + + return ( + + {title} + {translate('signerInfoStep.regulationRequiresUs')} + + + ); +} + +DirectorCheck.displayName = 'DirectorCheck'; + +export default DirectorCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx new file mode 100644 index 000000000000..338820c78cf1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx @@ -0,0 +1,83 @@ +import {Str} from 'expensify-common'; +import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; + +type EnterEmailProps = { + onSubmit: () => void; + + isUserDirector: boolean; +}; + +const {SIGNER_EMAIL, SECOND_SIGNER_EMAIL} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; + +function EnterEmail({onSubmit, isUserDirector}: EnterEmailProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const currency = policy?.outputCurrency ?? ''; + const shouldGatherBothEmails = currency === CONST.CURRENCY.AUD && !isUserDirector; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, shouldGatherBothEmails ? [SIGNER_EMAIL, SECOND_SIGNER_EMAIL] : [SIGNER_EMAIL]); + if (values[SIGNER_EMAIL] && !Str.isValidEmail(values[SIGNER_EMAIL])) { + errors[SIGNER_EMAIL] = translate('bankAccount.error.firstName'); + } + + if (shouldGatherBothEmails && values[SECOND_SIGNER_EMAIL] && !Str.isValidEmail(values[SECOND_SIGNER_EMAIL])) { + errors[SECOND_SIGNER_EMAIL] = translate('bankAccount.error.lastName'); + } + + return errors; + }, + [shouldGatherBothEmails, translate], + ); + + return ( + + {translate(shouldGatherBothEmails ? 'signerInfoStep.enterTwoEmails' : 'signerInfoStep.enterOneEmail')} + + {shouldGatherBothEmails && ( + + )} + + ); +} + +EnterEmail.displayName = 'EnterEmail'; + +export default EnterEmail; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx new file mode 100644 index 000000000000..15fea5e46691 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/HangTight.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function HangTight({tempSubmit}: {tempSubmit: () => void}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const handleSendReminder = () => { + // TODO remove that + tempSubmit(); + }; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + + + + {translate('signerInfoStep.hangTight')} + {translate('signerInfoStep.weAreWaiting')} + + + + + + + + + + + + @@ -402,6 +524,7 @@

Get Started

+ - + \ No newline at end of file From 5cb6da7957462823a46876e8cc9524bace9986b4 Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 13:16:50 -0800 Subject: [PATCH 285/421] Split up files --- help/_layouts/default.html | 668 +------------------------------------ help/default.css | 373 +++++++++++++++++++++ help/default.js | 289 ++++++++++++++++ 3 files changed, 664 insertions(+), 666 deletions(-) create mode 100644 help/default.css create mode 100644 help/default.js diff --git a/help/_layouts/default.html b/help/_layouts/default.html index 0dc3a3b5f9c0..906dacd9f2b4 100644 --- a/help/_layouts/default.html +++ b/help/_layouts/default.html @@ -5,381 +5,7 @@ {{ page.title }} - + @@ -525,297 +151,7 @@

Get Started

- + \ No newline at end of file diff --git a/help/default.css b/help/default.css new file mode 100644 index 000000000000..c71cbafcab75 --- /dev/null +++ b/help/default.css @@ -0,0 +1,373 @@ +body { + margin: 0; + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif; + background-color: #f9fafb; + color: #333; +} + +/* Header styling */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + background-color: #fff; + padding: 15px 0px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + width: 100%; + z-index: 1000; +} + +.logo { + display: flex; + align-items: center; + font-size: 24px; + font-weight: bold; + color: #0366d6; + margin-left: 20px; +} + +.logo a { + text-decoration: none; + color: inherit; +} + +/* Dropdown styling */ +.dropdown { + position: relative; + display: inline-block; + font-size: 24px; /* Match the logo font size */ + font-weight: normal; + text-decoration: underline; + color: #0366d6; + cursor: pointer; +} + +.dropdown-content { + display: none; + position: absolute; + background-color: #fff; + min-width: 160px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + z-index: 1; + padding: 12px 16px; +} + +.dropdown:hover .dropdown-content { + display: block; +} + +.dropdown a { + text-decoration: none; + color: #0366d6; + padding: 8px 0; + display: block; +} + +.dropdown a.active { + font-weight: bold; + background-color: #eaf5ff; +} + +/* Sidebar navigation for TOC */ +.toc-sidebar { + width: 250px; + position: fixed; + top: 60px; + bottom: 0; + left: 0; + background-color: #fff; + padding: 20px; + overflow-y: auto; + border-right: 1px solid #eaecef; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); + transform: translateY(0); + transition: transform 0.3s ease-in-out; + z-index: 999; +} + +.toc-sidebar.open { + transform: translateY(0); +} + +.toc-link::before { + display: none; +} + +.toc-sidebar ul { + list-style: none; + padding-left: 0; + line-height: 1.0; + font-size: 15px; +} + +.toc-sidebar li { + margin-left: 0; + padding-left: 0; +} + +.js-toc > a { + font-weight: bold; + font-size: 18px; + +} + +.js-toc > ul > li { + margin-top: 25px; +} + +.js-toc > ul > li > a { + font-weight: bold; +} + +.js-toc > ul > li > ul > li > ul > li { + padding-left: 10px; +} + +.toc-sidebar a { + word-wrap: break-word; + display: block; + padding: 1px 10px; + margin-bottom: 5px; + text-decoration: none; + color: #0366d6; + border-radius: 6px; +} + +.toc-sidebar a:hover { + background-color: #f1f8ff; + text-decoration: none; +} + +.toc-sidebar .is-active-link { + background-color: #eaf5ff; + color: #0366d6; + border-radius: 6px; +} + +/* Main content area */ +main { + margin-left: 300px; + padding: 20px; + flex-grow: 1; + max-width: 900px; +} + +main h1 { + display: none; +} + +main h2 { + font-size: 28px; + margin-bottom: 16px; +} + +main h3 { + font-size: 24px; + margin-bottom: 12px; +} + +main h4 { + font-size: 20px; + margin-bottom: 12px; +} + +main p { + font-size: 16px; + line-height: 1.6; + margin-bottom: 20px; +} + +.is-active-link { + font-weight: normal; +} + +.scroll-spacer { + height: 300px; +} + +/* Footer */ +footer { + margin-left: 300px; + color: #0366d6; + background-color: #f9fafb; + padding: 40px 20px; + font-size: 14px; + max-width: 900px; +} + +footer h3 { + color: #0366d6; +} + +footer ul { + list-style: none; + padding: 0; +} + +footer ul li a { + color: #0366d6; + text-decoration: none; +} + +footer ul li a:hover { + text-decoration: underline; +} + +footer .social-icons a img { + width: 20px; + margin-right: 10px; +} + +.footer-container { + display: flex; + justify-content: center; + max-width: 1200px; + margin: 0 auto; +} + +.footer-column { + flex: 1; + max-width: 300px; /* Set a max-width for each column */ + padding: 0 20px; /* Add padding for some space between the columns */ +} + +/* Mobile Styles */ +.hamburger { + display: none; + cursor: pointer; + flex-direction: column; + justify-content: space-between; + width: 24px; + height: 18px; +} + +.bar { + height: 3px; + width: 100%; + background-color: #0366d6; + border-radius: 10px; +} + +@media (max-width: 768px) { + + .hamburger { + display: flex; + margin-right: 20px; + } + + .toc-sidebar { + transform: translateY(-100%); + width: 100%; + } + + main { + margin-left: 0; + padding: 20px; + max-width: 100%; + } + + footer { + display: none; + } +} + +/* Modal background */ +#search-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + z-index: 1000; + display: flex; + justify-content: center; + align-items: flex-start; +} + +/* Modal content */ +#modal-content { + top: 20%; + background: white; + padding: 20px; + width: 60%; + max-width: 600px; + position: relative; + border-radius: 6px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); +} + +/* Search input with magnifying glass */ +.search-icon-wrapper { + display: flex; + align-items: center; + width: 100%; + padding: 8px; + border: 1px solid #eaecef; + border-radius: 6px; + box-sizing: border-box; +} + +.search-icon { + font-size: 18px; + margin-right: 8px; +} + +/* Updated input style */ +#search-input { + width: 100%; + border: none; + outline: none; + font-size: 20px; +} + +/* Search results */ +#search-results { + margin-top: 20px; + max-height: 300px; + overflow-y: auto; + border-top: 1px solid #eaecef; + padding-top: 10px; + outline: none; /* Disable the strong outline */ +} + +.search-result { + margin-bottom: 15px; + padding-bottom: 10px; + border-bottom: 1px solid #eaecef; +} + +.search-result-title { + font-weight: bold; + margin-bottom: 5px; + color: #0366d6; + text-decoration: none; +} + +.search-result-title:hover { + text-decoration: underline; +} + +.search-result-context { + font-size: 14px; + color: #586069; +} + +/* Highlighted search result */ +.highlight { + background-color: #eaecef; +} + +/* Softer focus style for search results */ +#search-results:focus { + border: 2px solid #ddd; /* Softer border on focus */ + outline: none; +} + +/* Soft yellow highlight for selected section */ +.highlight-section { + background-color: #fffbcc; + transition: background-color 0.3s ease; +} \ No newline at end of file diff --git a/help/default.js b/help/default.js new file mode 100644 index 000000000000..b92433e121ac --- /dev/null +++ b/help/default.js @@ -0,0 +1,289 @@ +function updateURLHashOnScroll() { + const activeLink = document.querySelector('.toc-sidebar .is-active-link'); + if (activeLink) { + const hash = activeLink.getAttribute('href'); + if (history.pushState) { + history.pushState(null, null, hash); + } else { + window.location.hash = hash; + } + } +} + +function scrollActiveLinkIntoView() { + const activeLink = document.querySelector('.toc-sidebar .is-active-link'); + if (activeLink) { + activeLink.scrollIntoView({ + behavior: 'smooth', + block: 'nearest' + }); + } +} + +tocbot.init({ + tocSelector: '.js-toc', + contentSelector: '.js-toc-content', + headingSelector: 'h1, h2, h3, h4, h5, h6', + collapseDepth: 4, + orderedList: false, + scrollSmooth: true, + scrollToActive: true, + enableUrlHashUpdateOnScroll: false, + headingsOffset: 80, + scrollSmoothOffset: -80, + tocScrollingWrapper: document.querySelector('.toc-sidebar'), + tocScrollOffset: 80, + headingObjectCallback: function(obj, element) { + const tocTitle = element.getAttribute('data-toc-title'); + if (tocTitle) { + obj.textContent = tocTitle; + } + return obj; + }, + onClick: function(e) { + setTimeout(scrollActiveLinkIntoView, 300); + document.getElementById('toc-sidebar').classList.remove('open'); // Close TOC after clicking + }, + scrollEndCallback: function() { + updateURLHashOnScroll(); + scrollActiveLinkIntoView(); + } +}); + +function adjustAnchorOnLoad() { + if (window.location.hash) { + const element = document.querySelector(window.location.hash); + if (element) { + window.scrollTo({ + top: element.getBoundingClientRect().top + window.pageYOffset - 80, + behavior: 'smooth' + }); + } + } +} + +window.addEventListener('load', adjustAnchorOnLoad); + +// Toggle sidebar on hamburger click +document.getElementById('hamburger').addEventListener('click', function() { + var sidebar = document.getElementById('toc-sidebar'); + sidebar.classList.toggle('open'); +}); + +// Keep track of the search results +let g_searchResultsArray = []; +let g_currentSelectionIndex = -1; +let g_highlightedSection = null; + +// Declare the index variable globally so it can be reused +let g_index = null; + +// Look up some commonly used elements once +const g_searchIcon = document.getElementById('search-icon'); +const g_searchModal = document.getElementById('search-modal'); +const g_searchResults = document.getElementById('search-results'); +const g_searchInput = document.getElementById('search-input'); + +// Show and initialize the search modal +function showSearchModal() { + g_searchModal.style.display = 'flex'; + if (g_searchResultsArray.length > 0) { + g_searchResults.style.display = 'block'; + g_searchResults.focus(); // Focus on search results if they exist + } else { + g_searchInput.focus(); + } +} + +// Open modal when search icon is clicked +g_searchIcon.addEventListener('click', showSearchModal); + +// Open modal when Cmd+K is pressed +document.addEventListener('keydown', function(event) { + if (event.metaKey && event.key === 'k') { + event.preventDefault(); + showSearchModal(); + } +}); + +// Close modal when pressing "Escape" +document.addEventListener('keydown', function(event) { + if (event.key === 'Escape' && g_searchModal.style.display === 'flex') { + g_searchModal.style.display = 'none'; + } +}); + +// Close modal when clicking outside of modal content +window.addEventListener('click', function(event) { + if (event.target == g_searchModal) { + g_searchModal.style.display = 'none'; + } +}); + +// Handle keyboard navigation (arrow keys and enter) +g_searchResults.addEventListener('keydown', function(event) { + // If the modal is being shown and has active search results, capture keydown + if (g_searchModal.style.display === 'flex' && g_searchResultsArray.length > 0) { + if (event.key === 'ArrowDown') { + event.preventDefault(); + selectNextResult(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + selectPreviousResult(); + } else if (event.key === 'Enter' && g_currentSelectionIndex >= 0) { + event.preventDefault(); + navigateToSelectedResult(); + } else if (!['Tab', 'Shift'].includes(event.key)) { + g_searchInput.focus(); // Focus back on input if typing occurs + } + } +}); + +function selectNextResult() { + if (g_currentSelectionIndex < g_searchResultsArray.length - 1) { + g_currentSelectionIndex++; + updateSelectedResult(); + } +} + +function selectPreviousResult() { + if (g_currentSelectionIndex > 0) { + g_currentSelectionIndex--; + updateSelectedResult(); + } +} + +function updateSelectedResult() { + g_searchResultsArray.forEach((result, index) => { + if (index === g_currentSelectionIndex) { + result.classList.add('highlight'); + result.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } else { + result.classList.remove('highlight'); + } + }); +} + +function navigateToSelectedResult() { + const selectedResult = g_searchResultsArray[g_currentSelectionIndex]; + const link = selectedResult.querySelector('a'); + if (link) { + link.click(); + } +} + +// Execute search when pressing "Enter" in the input field +g_searchInput.addEventListener('keydown', function(event) { + if (event.key === 'Enter') { + event.preventDefault(); + g_searchSubmit.click(); + } +}); + +// Perform search when search button is clicked +const g_searchSubmit = document.getElementById('search-submit'); + +// Handle submitting the search query +g_searchSubmit.addEventListener('click', async function() { + const query = g_searchInput.value.trim().toLowerCase(); + g_searchResults.innerHTML = ''; + g_currentSelectionIndex = -1; + + if (query.length === 0) { + g_searchResults.style.display = 'none'; + return; + } + + // Load the JSON search index file, if not already defined + if (g_index === null) { + console.log('Loading search index from:', '/searchIndex.json'); + const response = await fetch('/searchIndex.json'); + const indexData = await response.json(); + g_index = new FlexSearch.Document({ + document: { + id: 'id', + index: ['content'], // Index on the content field + store: ['title', 'url', 'content'], // Store title, URL, and content + } + }); + + // Import the index + for (const [key, data] of Object.entries(indexData)) { + await g_index.import(key, data); + } + } else { + console.log('Reusing existing search index'); + } + + // Perform search + const results = await g_index.search({ + query, + field: 'content' + }); + + if (results.length > 0) { + g_searchResultsArray = []; + results.forEach(result => { + result.result.forEach(docId => { + const doc = g_index.store[docId]; + if (doc && doc.content) { + const searchTermIndex = doc.content.toLowerCase().indexOf(query); + const contextBefore = doc.content.substring(Math.max(0, searchTermIndex - 30), searchTermIndex); + const contextAfter = doc.content.substring(searchTermIndex + query.length, Math.min(doc.content.length, searchTermIndex + query.length + 30)); + const searchResultHtml = ` +
+ ${doc.title} +
...${contextBefore}${query}${contextAfter}...
+
+ `; + const resultElement = document.createElement('div'); + resultElement.innerHTML = searchResultHtml; + g_searchResults.appendChild(resultElement); + g_searchResultsArray.push(resultElement); + + // Automatically select the first result + if (g_searchResultsArray.length === 1) { + g_currentSelectionIndex = 0; + updateSelectedResult(); + } + } + }); + }); + g_searchResults.style.display = 'block'; + g_searchResults.focus(); // Focus on search results whenever there are results + } else { + g_searchResults.style.display = 'none'; + } +}); + +// Trigger the TOC link click to use TocBot's smooth scrolling behavior +function scrollToTOC(url) { + const elementId = url.split('#')[1]; + const tocLink = document.querySelector(`.toc-sidebar a[href="#${elementId}"]`); + + if (tocLink) { + tocLink.click(); // Simulate a click on the TOC link for smooth scroll + highlightSelectedSection(elementId); // Highlight the section in yellow + closeModalAfterClick(); + } +} + +// Highlight the selected section +function highlightSelectedSection(sectionId) { + // Remove the previous highlight, if any + if (g_highlightedSection) { + g_highlightedSection.classList.remove('highlight-section'); + } + + // Highlight the new section + const sectionElement = document.getElementById(sectionId); + if (sectionElement) { + sectionElement.classList.add('highlight-section'); + g_highlightedSection = sectionElement; + } +} + +// Close modal after clicking a search result +function closeModalAfterClick() { + g_searchModal.style.display = 'none'; +} \ No newline at end of file From efdebefab9c4a6b1d4a75d567c352ca3a9cd100c Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 14:23:21 -0800 Subject: [PATCH 286/421] Prettified the JS --- help/default.js | 86 ++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/help/default.js b/help/default.js index b92433e121ac..0884de48bab2 100644 --- a/help/default.js +++ b/help/default.js @@ -15,39 +15,39 @@ function scrollActiveLinkIntoView() { if (activeLink) { activeLink.scrollIntoView({ behavior: 'smooth', - block: 'nearest' + block: 'nearest', }); } } tocbot.init({ - tocSelector: '.js-toc', - contentSelector: '.js-toc-content', - headingSelector: 'h1, h2, h3, h4, h5, h6', - collapseDepth: 4, - orderedList: false, - scrollSmooth: true, - scrollToActive: true, - enableUrlHashUpdateOnScroll: false, - headingsOffset: 80, - scrollSmoothOffset: -80, - tocScrollingWrapper: document.querySelector('.toc-sidebar'), - tocScrollOffset: 80, - headingObjectCallback: function(obj, element) { - const tocTitle = element.getAttribute('data-toc-title'); - if (tocTitle) { - obj.textContent = tocTitle; - } - return obj; - }, - onClick: function(e) { - setTimeout(scrollActiveLinkIntoView, 300); - document.getElementById('toc-sidebar').classList.remove('open'); // Close TOC after clicking - }, - scrollEndCallback: function() { - updateURLHashOnScroll(); - scrollActiveLinkIntoView(); - } + tocSelector: '.js-toc', + contentSelector: '.js-toc-content', + headingSelector: 'h1, h2, h3, h4, h5, h6', + collapseDepth: 4, + orderedList: false, + scrollSmooth: true, + scrollToActive: true, + enableUrlHashUpdateOnScroll: false, + headingsOffset: 80, + scrollSmoothOffset: -80, + tocScrollingWrapper: document.querySelector('.toc-sidebar'), + tocScrollOffset: 80, + headingObjectCallback: function (obj, element) { + const tocTitle = element.getAttribute('data-toc-title'); + if (tocTitle) { + obj.textContent = tocTitle; + } + return obj; + }, + onClick: function (e) { + setTimeout(scrollActiveLinkIntoView, 300); + document.getElementById('toc-sidebar').classList.remove('open'); // Close TOC after clicking + }, + scrollEndCallback: function () { + updateURLHashOnScroll(); + scrollActiveLinkIntoView(); + }, }); function adjustAnchorOnLoad() { @@ -56,7 +56,7 @@ function adjustAnchorOnLoad() { if (element) { window.scrollTo({ top: element.getBoundingClientRect().top + window.pageYOffset - 80, - behavior: 'smooth' + behavior: 'smooth', }); } } @@ -65,7 +65,7 @@ function adjustAnchorOnLoad() { window.addEventListener('load', adjustAnchorOnLoad); // Toggle sidebar on hamburger click -document.getElementById('hamburger').addEventListener('click', function() { +document.getElementById('hamburger').addEventListener('click', function () { var sidebar = document.getElementById('toc-sidebar'); sidebar.classList.toggle('open'); }); @@ -99,7 +99,7 @@ function showSearchModal() { g_searchIcon.addEventListener('click', showSearchModal); // Open modal when Cmd+K is pressed -document.addEventListener('keydown', function(event) { +document.addEventListener('keydown', function (event) { if (event.metaKey && event.key === 'k') { event.preventDefault(); showSearchModal(); @@ -107,21 +107,21 @@ document.addEventListener('keydown', function(event) { }); // Close modal when pressing "Escape" -document.addEventListener('keydown', function(event) { +document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && g_searchModal.style.display === 'flex') { g_searchModal.style.display = 'none'; } }); // Close modal when clicking outside of modal content -window.addEventListener('click', function(event) { +window.addEventListener('click', function (event) { if (event.target == g_searchModal) { g_searchModal.style.display = 'none'; } }); // Handle keyboard navigation (arrow keys and enter) -g_searchResults.addEventListener('keydown', function(event) { +g_searchResults.addEventListener('keydown', function (event) { // If the modal is being shown and has active search results, capture keydown if (g_searchModal.style.display === 'flex' && g_searchResultsArray.length > 0) { if (event.key === 'ArrowDown') { @@ -157,7 +157,7 @@ function updateSelectedResult() { g_searchResultsArray.forEach((result, index) => { if (index === g_currentSelectionIndex) { result.classList.add('highlight'); - result.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + result.scrollIntoView({behavior: 'smooth', block: 'nearest'}); } else { result.classList.remove('highlight'); } @@ -173,7 +173,7 @@ function navigateToSelectedResult() { } // Execute search when pressing "Enter" in the input field -g_searchInput.addEventListener('keydown', function(event) { +g_searchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); g_searchSubmit.click(); @@ -184,7 +184,7 @@ g_searchInput.addEventListener('keydown', function(event) { const g_searchSubmit = document.getElementById('search-submit'); // Handle submitting the search query -g_searchSubmit.addEventListener('click', async function() { +g_searchSubmit.addEventListener('click', async function () { const query = g_searchInput.value.trim().toLowerCase(); g_searchResults.innerHTML = ''; g_currentSelectionIndex = -1; @@ -204,7 +204,7 @@ g_searchSubmit.addEventListener('click', async function() { id: 'id', index: ['content'], // Index on the content field store: ['title', 'url', 'content'], // Store title, URL, and content - } + }, }); // Import the index @@ -218,13 +218,13 @@ g_searchSubmit.addEventListener('click', async function() { // Perform search const results = await g_index.search({ query, - field: 'content' + field: 'content', }); if (results.length > 0) { g_searchResultsArray = []; - results.forEach(result => { - result.result.forEach(docId => { + results.forEach((result) => { + result.result.forEach((docId) => { const doc = g_index.store[docId]; if (doc && doc.content) { const searchTermIndex = doc.content.toLowerCase().indexOf(query); @@ -250,7 +250,7 @@ g_searchSubmit.addEventListener('click', async function() { }); }); g_searchResults.style.display = 'block'; - g_searchResults.focus(); // Focus on search results whenever there are results + g_searchResults.focus(); // Focus on search results whenever there are results } else { g_searchResults.style.display = 'none'; } @@ -286,4 +286,4 @@ function highlightSelectedSection(sectionId) { // Close modal after clicking a search result function closeModalAfterClick() { g_searchModal.style.display = 'none'; -} \ No newline at end of file +} From cdb71f320b46b1ce68fd7c04ab2731dcd08f1b16 Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 14:48:11 -0800 Subject: [PATCH 287/421] Fixed a lot of lint errors --- help/default.js | 123 ++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 68 deletions(-) diff --git a/help/default.js b/help/default.js index 0884de48bab2..ba85a8306680 100644 --- a/help/default.js +++ b/help/default.js @@ -1,12 +1,11 @@ function updateURLHashOnScroll() { const activeLink = document.querySelector('.toc-sidebar .is-active-link'); - if (activeLink) { - const hash = activeLink.getAttribute('href'); - if (history.pushState) { - history.pushState(null, null, hash); - } else { - window.location.hash = hash; - } + if (!activeLink) return; + const hash = activeLink.getAttribute('href'); + if (window.history.pushState) { + window.history.pushState(null, null, hash); + } else { + window.location.hash = hash; } } @@ -20,7 +19,8 @@ function scrollActiveLinkIntoView() { } } -tocbot.init({ +// Assuming tocbot is globally defined +window.tocbot.init({ tocSelector: '.js-toc', contentSelector: '.js-toc-content', headingSelector: 'h1, h2, h3, h4, h5, h6', @@ -33,32 +33,33 @@ tocbot.init({ scrollSmoothOffset: -80, tocScrollingWrapper: document.querySelector('.toc-sidebar'), tocScrollOffset: 80, - headingObjectCallback: function (obj, element) { + headingObjectCallback(obj, element) { const tocTitle = element.getAttribute('data-toc-title'); if (tocTitle) { - obj.textContent = tocTitle; + const newObj = { ...obj }; + newObj.textContent = tocTitle; + return newObj; } return obj; }, - onClick: function (e) { + onClick() { setTimeout(scrollActiveLinkIntoView, 300); - document.getElementById('toc-sidebar').classList.remove('open'); // Close TOC after clicking + document.getElementById('toc-sidebar').classList.remove('open'); }, - scrollEndCallback: function () { + scrollEndCallback() { updateURLHashOnScroll(); scrollActiveLinkIntoView(); }, }); function adjustAnchorOnLoad() { - if (window.location.hash) { - const element = document.querySelector(window.location.hash); - if (element) { - window.scrollTo({ - top: element.getBoundingClientRect().top + window.pageYOffset - 80, - behavior: 'smooth', - }); - } + if (!window.location.hash) return; + const element = document.querySelector(window.location.hash); + if (element) { + window.scrollTo({ + top: element.getBoundingClientRect().top + window.pageYOffset - 80, + behavior: 'smooth', + }); } } @@ -66,7 +67,7 @@ window.addEventListener('load', adjustAnchorOnLoad); // Toggle sidebar on hamburger click document.getElementById('hamburger').addEventListener('click', function () { - var sidebar = document.getElementById('toc-sidebar'); + let sidebar = document.getElementById('toc-sidebar'); sidebar.classList.toggle('open'); }); @@ -89,7 +90,7 @@ function showSearchModal() { g_searchModal.style.display = 'flex'; if (g_searchResultsArray.length > 0) { g_searchResults.style.display = 'block'; - g_searchResults.focus(); // Focus on search results if they exist + g_searchResults.focus(); } else { g_searchInput.focus(); } @@ -115,42 +116,38 @@ document.addEventListener('keydown', function (event) { // Close modal when clicking outside of modal content window.addEventListener('click', function (event) { - if (event.target == g_searchModal) { + if (event.target === g_searchModal) { g_searchModal.style.display = 'none'; } }); // Handle keyboard navigation (arrow keys and enter) g_searchResults.addEventListener('keydown', function (event) { - // If the modal is being shown and has active search results, capture keydown - if (g_searchModal.style.display === 'flex' && g_searchResultsArray.length > 0) { - if (event.key === 'ArrowDown') { - event.preventDefault(); - selectNextResult(); - } else if (event.key === 'ArrowUp') { - event.preventDefault(); - selectPreviousResult(); - } else if (event.key === 'Enter' && g_currentSelectionIndex >= 0) { - event.preventDefault(); - navigateToSelectedResult(); - } else if (!['Tab', 'Shift'].includes(event.key)) { - g_searchInput.focus(); // Focus back on input if typing occurs - } + if (g_searchModal.style.display !== 'flex' || g_searchResultsArray.length === 0) return; + if (event.key === 'ArrowDown') { + event.preventDefault(); + selectNextResult(); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + selectPreviousResult(); + } else if (event.key === 'Enter' && g_currentSelectionIndex >= 0) { + event.preventDefault(); + navigateToSelectedResult(); + } else if (!['Tab', 'Shift'].includes(event.key)) { + g_searchInput.focus(); } }); function selectNextResult() { - if (g_currentSelectionIndex < g_searchResultsArray.length - 1) { - g_currentSelectionIndex++; - updateSelectedResult(); - } + if (g_currentSelectionIndex >= g_searchResultsArray.length - 1) return; + g_currentSelectionIndex++; + updateSelectedResult(); } function selectPreviousResult() { - if (g_currentSelectionIndex > 0) { - g_currentSelectionIndex--; - updateSelectedResult(); - } + if (g_currentSelectionIndex <= 0) return; + g_currentSelectionIndex--; + updateSelectedResult(); } function updateSelectedResult() { @@ -176,15 +173,13 @@ function navigateToSelectedResult() { g_searchInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); - g_searchSubmit.click(); + document.getElementById('search-submit').click(); } }); // Perform search when search button is clicked -const g_searchSubmit = document.getElementById('search-submit'); - -// Handle submitting the search query -g_searchSubmit.addEventListener('click', async function () { +// Assuming search-submit is defined +document.getElementById('search-submit').addEventListener('click', async function () { const query = g_searchInput.value.trim().toLowerCase(); g_searchResults.innerHTML = ''; g_currentSelectionIndex = -1; @@ -194,28 +189,23 @@ g_searchSubmit.addEventListener('click', async function () { return; } - // Load the JSON search index file, if not already defined if (g_index === null) { - console.log('Loading search index from:', '/searchIndex.json'); + // Assuming the fetch works correctly without console.log const response = await fetch('/searchIndex.json'); const indexData = await response.json(); - g_index = new FlexSearch.Document({ + g_index = new window.FlexSearch.Document({ document: { id: 'id', - index: ['content'], // Index on the content field - store: ['title', 'url', 'content'], // Store title, URL, and content + index: ['content'], + store: ['title', 'url', 'content'], }, }); - // Import the index for (const [key, data] of Object.entries(indexData)) { - await g_index.import(key, data); + g_index.import(key, data); } - } else { - console.log('Reusing existing search index'); } - // Perform search const results = await g_index.search({ query, field: 'content', @@ -241,7 +231,6 @@ g_searchSubmit.addEventListener('click', async function () { g_searchResults.appendChild(resultElement); g_searchResultsArray.push(resultElement); - // Automatically select the first result if (g_searchResultsArray.length === 1) { g_currentSelectionIndex = 0; updateSelectedResult(); @@ -250,7 +239,7 @@ g_searchSubmit.addEventListener('click', async function () { }); }); g_searchResults.style.display = 'block'; - g_searchResults.focus(); // Focus on search results whenever there are results + g_searchResults.focus(); } else { g_searchResults.style.display = 'none'; } @@ -262,20 +251,18 @@ function scrollToTOC(url) { const tocLink = document.querySelector(`.toc-sidebar a[href="#${elementId}"]`); if (tocLink) { - tocLink.click(); // Simulate a click on the TOC link for smooth scroll - highlightSelectedSection(elementId); // Highlight the section in yellow + tocLink.click(); + highlightSelectedSection(elementId); closeModalAfterClick(); } } // Highlight the selected section function highlightSelectedSection(sectionId) { - // Remove the previous highlight, if any if (g_highlightedSection) { g_highlightedSection.classList.remove('highlight-section'); } - // Highlight the new section const sectionElement = document.getElementById(sectionId); if (sectionElement) { sectionElement.classList.add('highlight-section'); @@ -286,4 +273,4 @@ function highlightSelectedSection(sectionId) { // Close modal after clicking a search result function closeModalAfterClick() { g_searchModal.style.display = 'none'; -} +} \ No newline at end of file From 7b4eebf90465c5f4425ccbd0ed05c7b2aeb7871e Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 14:52:18 -0800 Subject: [PATCH 288/421] Prettied the code --- help/default.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/help/default.js b/help/default.js index ba85a8306680..d8e75413597a 100644 --- a/help/default.js +++ b/help/default.js @@ -36,7 +36,7 @@ window.tocbot.init({ headingObjectCallback(obj, element) { const tocTitle = element.getAttribute('data-toc-title'); if (tocTitle) { - const newObj = { ...obj }; + const newObj = {...obj}; newObj.textContent = tocTitle; return newObj; } @@ -273,4 +273,4 @@ function highlightSelectedSection(sectionId) { // Close modal after clicking a search result function closeModalAfterClick() { g_searchModal.style.display = 'none'; -} \ No newline at end of file +} From e8e3ce21848519e0737163c390b503d498578e48 Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 15:35:38 -0800 Subject: [PATCH 289/421] fixing more lint/pretty issues --- help/default.js | 80 ++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/help/default.js b/help/default.js index d8e75413597a..84a8991ac9f2 100644 --- a/help/default.js +++ b/help/default.js @@ -1,6 +1,8 @@ function updateURLHashOnScroll() { const activeLink = document.querySelector('.toc-sidebar .is-active-link'); - if (!activeLink) return; + if (!activeLink) { + return; + } const hash = activeLink.getAttribute('href'); if (window.history.pushState) { window.history.pushState(null, null, hash); @@ -53,7 +55,9 @@ window.tocbot.init({ }); function adjustAnchorOnLoad() { - if (!window.location.hash) return; + if (!window.location.hash) { + return; + } const element = document.querySelector(window.location.hash); if (element) { window.scrollTo({ @@ -67,7 +71,7 @@ window.addEventListener('load', adjustAnchorOnLoad); // Toggle sidebar on hamburger click document.getElementById('hamburger').addEventListener('click', function () { - let sidebar = document.getElementById('toc-sidebar'); + const sidebar = document.getElementById('toc-sidebar'); sidebar.classList.toggle('open'); }); @@ -123,7 +127,9 @@ window.addEventListener('click', function (event) { // Handle keyboard navigation (arrow keys and enter) g_searchResults.addEventListener('keydown', function (event) { - if (g_searchModal.style.display !== 'flex' || g_searchResultsArray.length === 0) return; + if (g_searchModal.style.display !== 'flex' || g_searchResultsArray.length === 0) { + return; + } if (event.key === 'ArrowDown') { event.preventDefault(); selectNextResult(); @@ -139,13 +145,17 @@ g_searchResults.addEventListener('keydown', function (event) { }); function selectNextResult() { - if (g_currentSelectionIndex >= g_searchResultsArray.length - 1) return; + if (g_currentSelectionIndex >= g_searchResultsArray.length - 1) { + return; + } g_currentSelectionIndex++; updateSelectedResult(); } function selectPreviousResult() { - if (g_currentSelectionIndex <= 0) return; + if (g_currentSelectionIndex <= 0) { + return; + } g_currentSelectionIndex--; updateSelectedResult(); } @@ -179,7 +189,7 @@ g_searchInput.addEventListener('keydown', function (event) { // Perform search when search button is clicked // Assuming search-submit is defined -document.getElementById('search-submit').addEventListener('click', async function () { +document.getElementById('search-submit').addEventListener('click', function () { const query = g_searchInput.value.trim().toLowerCase(); g_searchResults.innerHTML = ''; g_currentSelectionIndex = -1; @@ -191,27 +201,35 @@ document.getElementById('search-submit').addEventListener('click', async functio if (g_index === null) { // Assuming the fetch works correctly without console.log - const response = await fetch('/searchIndex.json'); - const indexData = await response.json(); - g_index = new window.FlexSearch.Document({ - document: { - id: 'id', - index: ['content'], - store: ['title', 'url', 'content'], - }, - }); + fetch('/searchIndex.json') + .then((response) => response.json()) + .then((indexData) => { + g_index = new window.FlexSearch.Document({ + document: { + id: 'id', + index: ['content'], + store: ['title', 'url', 'content'], + }, + }); - for (const [key, data] of Object.entries(indexData)) { - g_index.import(key, data); - } + for (const [key, data] of Object.entries(indexData)) { + g_index.import(key, data); + } + + performSearch(query); + }); + } else { + performSearch(query); } +}); - const results = await g_index.search({ +function performSearch(query) { + const results = g_index.search({ query, field: 'content', }); - if (results.length > 0) { + if (results && results.length > 0) { g_searchResultsArray = []; results.forEach((result) => { result.result.forEach((docId) => { @@ -221,10 +239,10 @@ document.getElementById('search-submit').addEventListener('click', async functio const contextBefore = doc.content.substring(Math.max(0, searchTermIndex - 30), searchTermIndex); const contextAfter = doc.content.substring(searchTermIndex + query.length, Math.min(doc.content.length, searchTermIndex + query.length + 30)); const searchResultHtml = ` -
- ${doc.title} -
...${contextBefore}${query}${contextAfter}...
-
+
+ ${doc.title} +
...${contextBefore}${query}${contextAfter}...
+
`; const resultElement = document.createElement('div'); resultElement.innerHTML = searchResultHtml; @@ -243,18 +261,6 @@ document.getElementById('search-submit').addEventListener('click', async functio } else { g_searchResults.style.display = 'none'; } -}); - -// Trigger the TOC link click to use TocBot's smooth scrolling behavior -function scrollToTOC(url) { - const elementId = url.split('#')[1]; - const tocLink = document.querySelector(`.toc-sidebar a[href="#${elementId}"]`); - - if (tocLink) { - tocLink.click(); - highlightSelectedSection(elementId); - closeModalAfterClick(); - } } // Highlight the selected section From ad044a2b74e36c4836a5257cdc237e17d2462ec1 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 18 Nov 2024 00:48:28 +0100 Subject: [PATCH 290/421] feat: bring back firebase tracking for sidebar_loaded --- contributingGuides/PERFORMANCE_METRICS.md | 2 +- src/libs/actions/App.ts | 1 + src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/contributingGuides/PERFORMANCE_METRICS.md b/contributingGuides/PERFORMANCE_METRICS.md index ecebbaae4e0e..9e942f21d918 100644 --- a/contributingGuides/PERFORMANCE_METRICS.md +++ b/contributingGuides/PERFORMANCE_METRICS.md @@ -14,7 +14,7 @@ Project is using Firebase for tracking these metrics. However, not all of them a | `js_loaded` | ✅ | The time it takes for the JavaScript bundle to load.

**Platforms:** Android, iOS | **Android:** Starts in the `onCreate` method.

**iOS:** Starts in the AppDelegate's `didFinishLaunchingWithOptions` method. | Stops at the first render of the app via native module on the JS side. | | `_app_in_foreground` | ✅ | The time when the app is running in the foreground and available to the user.

**Platforms:** Android, iOS | **Android:** Starts when the first activity to reach the foreground has its `onResume()` method called.

**iOS:** Starts when the application receives the `UIApplicationDidBecomeActiveNotification` notification. | **Android:** Stops when the last activity to leave the foreground has its `onStop()` method called.

**iOS:** Stops when it receives the `UIApplicationWillResignActiveNotification` notification. | | `_app_in_background` | ✅ | Time when the app is running in the background.

**Platforms:** Android, iOS | **Android:** Starts when the last activity to leave the foreground has its `onStop()` method called.

**iOS:** Starts when the application receives the `UIApplicationWillResignActiveNotification` notification. | **Android:** Stops when the first activity to reach the foreground has its `onResume()` method called.

**iOS:** Stops when it receives the `UIApplicationDidBecomeActiveNotification` notification. | -| `sidebar_loaded` | ❌ | Time taken for the Sidebar to load.

**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the LHN finishes laying out. | +| `sidebar_loaded` | ✅ | Time taken for the Sidebar to load.

**Platforms:** All | Starts when the Sidebar is mounted. | Stops when the LHN finishes laying out. | | `calc_most_recent_last_modified_action` | ✅ | Time taken to find the most recently modified report action or report.

**Platforms:** All | Starts when the app reconnects to the network | Ends when the app reconnects to the network and the most recent report action or report is found. | | `open_search` | ✅ | Time taken to open up the Search Router.

**Platforms:** All | Starts when the Search Router icon in LHN is pressed. | Stops when the list of available options finishes laying out. | | `load_search_options` | ✅ | Time taken to generate the list of options used in the Search Router.

**Platforms:** All | Starts when the `getSearchOptions` function is called. | Stops when the list of available options is generated. | diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index f778405ee6e8..f1f46aee0a93 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -180,6 +180,7 @@ function setSidebarLoaded() { Onyx.set(ONYXKEYS.IS_SIDEBAR_LOADED, true); Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED); + Timing.end(CONST.TIMING.SIDEBAR_LOADED); } let appState: AppStateStatus; diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 057189ae22c1..0dcb1124ee3e 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -12,6 +12,7 @@ import TopBar from '@libs/Navigation/AppNavigator/createCustomBottomTabNavigator import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; +import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -24,6 +25,7 @@ function BaseSidebarScreen() { useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); + Timing.start(CONST.TIMING.SIDEBAR_LOADED); }, []); useEffect(() => { From 6bce953fe5d67d76705b32eb534a168cbca29968 Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 16:14:30 -0800 Subject: [PATCH 291/421] More lint fixes --- help/default.js | 42 ++++++++++++++---------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/help/default.js b/help/default.js index 84a8991ac9f2..3ebb4a37de5b 100644 --- a/help/default.js +++ b/help/default.js @@ -105,24 +105,27 @@ g_searchIcon.addEventListener('click', showSearchModal); // Open modal when Cmd+K is pressed document.addEventListener('keydown', function (event) { - if (event.metaKey && event.key === 'k') { - event.preventDefault(); - showSearchModal(); + if (!(event.metaKey && event.key === 'k')) { + return; } + event.preventDefault(); + showSearchModal(); }); // Close modal when pressing "Escape" document.addEventListener('keydown', function (event) { - if (event.key === 'Escape' && g_searchModal.style.display === 'flex') { - g_searchModal.style.display = 'none'; + if (!(event.key === 'Escape' && g_searchModal.style.display === 'flex')) { + return; } + g_searchModal.style.display = 'none'; }); // Close modal when clicking outside of modal content window.addEventListener('click', function (event) { - if (event.target === g_searchModal) { - g_searchModal.style.display = 'none'; + if (!(event.target === g_searchModal)) { + return; } + g_searchModal.style.display = 'none'; }); // Handle keyboard navigation (arrow keys and enter) @@ -181,10 +184,11 @@ function navigateToSelectedResult() { // Execute search when pressing "Enter" in the input field g_searchInput.addEventListener('keydown', function (event) { - if (event.key === 'Enter') { - event.preventDefault(); - document.getElementById('search-submit').click(); + if (!(event.key === 'Enter')) { + return; } + event.preventDefault(); + document.getElementById('search-submit').click(); }); // Perform search when search button is clicked @@ -262,21 +266,3 @@ function performSearch(query) { g_searchResults.style.display = 'none'; } } - -// Highlight the selected section -function highlightSelectedSection(sectionId) { - if (g_highlightedSection) { - g_highlightedSection.classList.remove('highlight-section'); - } - - const sectionElement = document.getElementById(sectionId); - if (sectionElement) { - sectionElement.classList.add('highlight-section'); - g_highlightedSection = sectionElement; - } -} - -// Close modal after clicking a search result -function closeModalAfterClick() { - g_searchModal.style.display = 'none'; -} From 4715f2e54208a2d37f38a9ebbfdab3a6bdef33d1 Mon Sep 17 00:00:00 2001 From: David Barrett Date: Sun, 17 Nov 2024 16:25:49 -0800 Subject: [PATCH 292/421] Even more lint fixes --- help/default.js | 1 - 1 file changed, 1 deletion(-) diff --git a/help/default.js b/help/default.js index 3ebb4a37de5b..1c27ade6c9e1 100644 --- a/help/default.js +++ b/help/default.js @@ -78,7 +78,6 @@ document.getElementById('hamburger').addEventListener('click', function () { // Keep track of the search results let g_searchResultsArray = []; let g_currentSelectionIndex = -1; -let g_highlightedSection = null; // Declare the index variable globally so it can be reused let g_index = null; From 6eaa9a8067e1fec6035e995f33e1f3386b7a1321 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:13:50 -0800 Subject: [PATCH 293/421] Replace "policy" with "workspace" in QuickBooks-Time.md --- docs/articles/expensify-classic/connections/QuickBooks-Time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/connections/QuickBooks-Time.md b/docs/articles/expensify-classic/connections/QuickBooks-Time.md index 5bbd2c4b583c..bcc06e171d4f 100644 --- a/docs/articles/expensify-classic/connections/QuickBooks-Time.md +++ b/docs/articles/expensify-classic/connections/QuickBooks-Time.md @@ -1,6 +1,6 @@ --- title: Expensify and TSheets/QuickBooks Time Integration Guide -description: This help document explains how to connect TSheets/QuickBooks Time to your Expensify policy +description: This help document explains how to connect TSheets/QuickBooks Time to your Expensify workspace --- # Overview From a9d46821f66af440947ee75c99ac1c8e41eaddb1 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:19:07 -0800 Subject: [PATCH 294/421] Update Set-up-your-individual-workspace.md to reflect "workspace" in lieu of "policy" --- .../workspaces/Set-up-your-individual-workspace.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md b/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md index c8be9a2728d5..04f2688eee90 100644 --- a/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md +++ b/docs/articles/expensify-classic/workspaces/Set-up-your-individual-workspace.md @@ -10,7 +10,7 @@ To set up your individual workspace, 1. Hover over Settings, then click **Workspaces**. 2. Click the **Individual** tab on the left. -3. Select the policy type that best fits your needs. +3. Select the workspace type that best fits your needs. 4. Set up your workspace details including the workspace name, expense rules, categories, and more. {% include info.html %} From b6b5bcc0e6ba71e274e7586ef4e331d763525f74 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:20:56 -0800 Subject: [PATCH 295/421] Update Company-Card-Settings.md to reflect "workspace" in lieu of "policy" --- .../company-cards/Company-Card-Settings.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md index 0fde76c8fa92..553171d73dde 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md +++ b/docs/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings.md @@ -29,9 +29,9 @@ Personal Liability: Users are allowed to delete company card expenses. If you update the settings on an existing company card feed, the changes will apply to expenses imported after the date the setting is saved. The update will not affect previously imported expenses. -## Preferred policy +## Preferred workspace -Setting a preferred policy for a company card feed will ensure that the imported transactions are added to a report on the policy you set. This setting is useful when members are on multiple policies and need to ensure their company card expenses are reported to a particular policy. +Setting a preferred workspace for a company card feed will ensure that the imported transactions are added to a report on the workspace you set. This setting is useful when members are on multiple workspaces and need to ensure their company card expenses are reported to a particular workspace. # How to use Scheduled Submit with company cards All expenses must be placed on a report if they need to be approved; with Scheduled Submit, you no longer need to worry about the arduous task of employees creating their expenses, adding them to a report, and submitting them manually. All they need to do is SmartScan their receipts and Concierge will take care of the rest, on a variety of schedules that you can set according to your preferences! @@ -41,11 +41,11 @@ Concierge won't automatically submit expenses on reports that have Expense Viola An employee can add comments in the Expense Comment field or at the bottom of the report to clarify any details. ## Enable Scheduled Submit -Scheduled Submit is enabled in the Group Policy by navigating to Settings > Policies > Group > Policy Name > Reports > Scheduled Submit +Scheduled Submit is enabled in the Group Workspace by navigating to Settings > Workspaces > Group > Workspace Name > Reports > Scheduled Submit Use the toggle to enable Scheduled Submit Choose your desired frequency -If Scheduled Submit is disabled on the group policy level (or set to a manual frequency), and you have noticed expense reports are still automatically submitted to the group policy, it's likely Scheduled Submit is enabled on the user’s Individual Policy settings. +If Scheduled Submit is disabled on the group workspace level (or set to a manual frequency), and you have noticed expense reports are still automatically submitted to the group workspace, it's likely Scheduled Submit is enabled on the user’s Individual Workspace settings. # How to connect company cards to an accounting integration @@ -59,7 +59,7 @@ You're all done. After the account is set, exported expenses will be mapped to t ## Pooled GL account To export credit card expenses to a pooled GL account: -Go to Settings > Policies > Group > Policy Name > Connections > Accounting Integrations > Configure +Go to Settings > Workspaces > Group > Workspace Name > Connections > Accounting Integrations > Configure Select Credit Card / Charge Card / Bank Transaction as your Non-reimbursable export option. Please review the Export Settings page for exporting Expense Reports to NetSuite Select the Vendor/liability account you want to export all non-reimbursable expenses to. @@ -86,7 +86,7 @@ It's important to note that eReceipts are not generated for lodging expenses. Mo {% include faq-begin.md %} ## What plan/subscription is required in order to manage corporate cards? -Group Policy (Collect or Control plan only) +Group Workspace (Collect or Control plan only) ## When do my company card transactions import to Expensify? Credit card transactions are imported to Expensify once they’re posted to the bank account. This usually takes 1-3 business days between the point of purchase and when the transactions populate in your account. From c76318c22d9fe09707b89205c26a3eef713d49d0 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:21:49 -0800 Subject: [PATCH 296/421] Update SAML-SSO.md to reflect "workspace" in lieu of "policy" --- docs/articles/expensify-classic/domains/SAML-SSO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/domains/SAML-SSO.md b/docs/articles/expensify-classic/domains/SAML-SSO.md index a6032afe8d24..da4bd5639120 100644 --- a/docs/articles/expensify-classic/domains/SAML-SSO.md +++ b/docs/articles/expensify-classic/domains/SAML-SSO.md @@ -88,7 +88,7 @@ Before getting started, you will need a verified domain and Control plan to set 6. The new trust is now created. Highlight the trust, then click *Edit claim rules* on the right. 7. Click *Add a Rule*. 8. The default option should be *Send LDAP Attributes as Claims*. Click Next. -9. Depending upon how your Active Directory is set up, you may or may not have a useful email address associated with each user, or you may have a policy to use the UPN as the user attribute for authentication. If so, using the UPN user attribute may be appropriate for you. If not, you can use the email address attribute. +9. Depending upon how your Active Directory is set up, you may or may not have a useful email address associated with each user, or you may have a workspace to use the UPN as the user attribute for authentication. If so, using the UPN user attribute may be appropriate for you. If not, you can use the email address attribute. 10. Give the rule a name like *Get email address from AD*. Choose Active Directory as the attribute store from the dropdown list. Choose your source user attribute to pass to Expensify that has users’ email address info in it, usually either *E-Mail-Address* or *User-Principal-Name*. Select the outgoing claim type as “E-Mail Address”. Click OK. 11. Add another rule; this time, we want to *Transform an Incoming Claim*. Click Next. 12. Name the rule *Send email address*. The Incoming claim type should be *E-Mail Address*. The outgoing claim type should be *Name ID*, and the outgoing name ID format should be *Email*. Click OK. From 7c9db1e97bf258dcc295842b117b43c965438ac8 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:22:54 -0800 Subject: [PATCH 297/421] Update Change-Plan-Or-Subscription.md to reflect "workspace" in lieu of "policy" --- .../expensify-billing/Change-Plan-Or-Subscription.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md index b245a26d10a0..0c0153522af3 100644 --- a/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md +++ b/docs/articles/expensify-classic/expensify-billing/Change-Plan-Or-Subscription.md @@ -80,7 +80,7 @@ Once you’ve successfully downgraded to a free Expensify account, your Workspac ## Will I be charged for a monthly subscription even if I don't use SmartScans? Yes, the Monthly Subscription is prepaid and not based on activity, so you'll be charged regardless of usage. -## I'm on a group policy; do I need the monthly subscription too? -Probably not. Group policy members already have unlimited SmartScans, so there's usually no need to buy the subscription. However, you can use it for personal use if you leave your company's Workspace. +## I'm on a group workspace; do I need the monthly subscription too? +Probably not. Group workspace members already have unlimited SmartScans, so there's usually no need to buy the subscription. However, you can use it for personal use if you leave your company's Workspace. {% include faq-end.md %} From 3144de591da282010b6b31baa862841082d16e37 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:23:46 -0800 Subject: [PATCH 298/421] Update Personal-Credit-Cards.md to reflect "workspace" in lieu of "policy" --- .../connect-credit-cards/Personal-Credit-Cards.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md b/docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md index 05149ebf868e..36717a421c67 100644 --- a/docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md +++ b/docs/articles/expensify-classic/connect-credit-cards/Personal-Credit-Cards.md @@ -8,7 +8,7 @@ Welcome to the world of effortless expense tracking! Connecting your personal cr ## How to connect your personal card to import expenses Importing your card or bank via Account Settings will: Automatically sync your bank/card transactions with your Expensify account. These will merge seamlessly with any SmartScanned expenses in your account. -Generate IRS-compliant eReceipts, provided your Policy Admin has enabled this feature. +Generate IRS-compliant eReceipts, provided your Workspace Admin has enabled this feature. Discover below the numerous ways to easily bring your personal card expenses into Expensify below. ### *Important terms to know:* @@ -45,7 +45,7 @@ _Please note: an OFX file type will require no editing but not all banks' OFX fi 6. Set the date format to match your CSV and adjust the currency to match your bank account currency. 7. If you've previously imported expenses for the same card, choose the default layout of a previously uploaded spreadsheet. 8. Scroll down and select which columns map to the merchant, date and amount (as a number without a currency symbol) – these are required presets which must be assigned. -9. If applicable, you can also map specific Categories and Tags as long as you don't have an integration connection to your default group policy. If you have an integration connected, you'll want to add the Categories and Tags to the expense after the expense is uploaded. +9. If applicable, you can also map specific Categories and Tags as long as you don't have an integration connection to your default group workspace. If you have an integration connected, you'll want to add the Categories and Tags to the expense after the expense is uploaded. 10. Check the preview of your selection under *Output Preview*. If everything looks good, you can then select *Add Expenses*. 11. For checking accounts, you may need to "Flip Amount Sign" as transactions are often exported as negative amounts. From 2888230032e9c945f7a58893bc15fca1ee5406d5 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:25:59 -0800 Subject: [PATCH 299/421] Update Configure-Netsuite.md to reflect "workspace" in lieu of "policy" --- .../connections/netsuite/Configure-Netsuite.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index aecf21acfc3f..068e4dd5bca9 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -40,18 +40,18 @@ The three options for the date your report will export with are: **Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite. -**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding policy. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills. +**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding workspace. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills. -**Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this policy. All the transactions will be posted to the payable account specified in the policy. You can also set an approval level in NetSuite for the journal entries. +**Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this workspace. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries. - Journal entry forms by default do not contain a customer column, so it is not possible to export customers or projects with this export option - The credit line and header level classifications are pulled from the employee record ## Export Settings for Non-Reimbursable Expenses -**Vendor Bills:** Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills. +**Vendor Bills:** Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your workspace's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills. -**Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. +**Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your workspace's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. - Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab - Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option From ac12ff6d13e4bd61d0251340af468203f8cd886a Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:27:34 -0800 Subject: [PATCH 300/421] Update Create-and-Pay-Bills.md to reflect "workspace" in lieu of "policy" --- .../bank-accounts-and-payments/payments/Create-and-Pay-Bills.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md index aff11c059d81..b231984f61e2 100644 --- a/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md +++ b/docs/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills.md @@ -11,7 +11,7 @@ You can receive bills in three ways: - Manual Upload: For physical bills, create a Bill in Expensify from the Reports page. # Bill Pay Workflow -1. When a vendor or supplier sends a bill to Expensify, the document is automatically SmartScanned, and a Bill is created. This Bill is managed by the primary domain contact, who can view it on the Reports page within their default group policy. +1. When a vendor or supplier sends a bill to Expensify, the document is automatically SmartScanned, and a Bill is created. This Bill is managed by the primary domain contact, who can view it on the Reports page within their default group workspace. 2. Once the Bill is ready for processing, it follows the established approval workflow. As each person approves it, the Bill appears in the next approver’s Inbox. The final approver will pay the Bill using one of the available payment methods. From eb3db9b2499dce334cabd5d8aac21072e3949d67 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:28:00 -0800 Subject: [PATCH 301/421] Update Enable-and-set-up-expense-violations.md to reflect "workspace" in lieu of "policy" --- .../workspaces/Enable-and-set-up-expense-violations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md index 7c3d8077c14d..1d5814138f6e 100644 --- a/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md +++ b/docs/articles/expensify-classic/workspaces/Enable-and-set-up-expense-violations.md @@ -29,7 +29,7 @@ If your workspace has automations set to automatically submit reports for approv - **Receipt required amount**: How much a single expense can cost before a receipt is required {% include info.html %} -Expensify includes certain system mandatory violations that can't be disabled, even if your policy has violations turned off. +Expensify includes certain system mandatory violations that can't be disabled, even if your workspace has violations turned off. {% include end-info.html %} # Set category rules From 714960442aeb35aa8562bdf29ba9ff115f9ec483 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:28:33 -0800 Subject: [PATCH 302/421] Update Netsuite-Troubleshooting.md to reflect "workspace" in lieu of "policy" --- .../connections/netsuite/Netsuite-Troubleshooting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md index 01aa21a28b80..6e3e9beef144 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/netsuite/Netsuite-Troubleshooting.md @@ -97,8 +97,8 @@ This can happen if the employee’s subsidiary in NetSuite doesn’t match what - Ensure the email on the employee record in NetSuite matches the email address of the report submitter in Expensify. - In NetSuite, make sure the employee's hire date is in the past and/or the termination date is in the future. 4. **Currency Match for Journal Entries:** - - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify policy all match. - - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/policy currency if necessary. + - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify workspace all match. + - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/workspace currency if necessary. # ExpensiError NS0024: Invalid Customer or Project Tag From 57e60a8b197a44131d19c281f137d7702a9a0ce0 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:28:54 -0800 Subject: [PATCH 303/421] Update Create-a-workspace-for-yourself.md to reflect "workspace" in lieu of "policy" --- .../getting-started/Create-a-workspace-for-yourself.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md b/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md index 69dea87ad8ea..5d64a9de3df5 100644 --- a/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md +++ b/docs/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself.md @@ -39,7 +39,7 @@ Here’s how to determine whether a personal or group workspace might be best fo
  1. Hover over Settings, then click Workspaces.
  2. Click the Individual tab on the left.
  3. -
  4. Select the policy type that best fits your needs.
  5. +
  6. Select the workspace type that best fits your needs.
  7. Set up your workspace details including the workspace name, expense rules, categories, and more.
From f3a2e762792d18b77acea83d63aa613ddb0322d6 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:30:20 -0800 Subject: [PATCH 304/421] Update Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md to reflect "workspace" in lieu of "policy" --- .../Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md index bded231d1daa..66466b57c854 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-Bootstrapped-Startups.md @@ -90,4 +90,4 @@ To view and pay bills: When you have bills to pay you can click *View all bills* under the *Manage your bills* box and we’ll keep a neatly organized list of all of the bills you can pay via ACH directly from your Expensify account. # You’re all set! -Congrats, you are all set up! If you need any assistance with anything mentioned above, reach out to either your Concierge directly in *[new.expensify.com](https://new.expensify.com/concierge)*, or email concierge@expensify.com. Create a Collect or Control Policy, and we’ll automatically assign a dedicated Setup Specialist to you. +Congrats, you are all set up! If you need any assistance with anything mentioned above, reach out to either your Concierge directly in *[new.expensify.com](https://new.expensify.com/concierge)*, or email concierge@expensify.com. Create a Collect or Control Workspace, and we’ll automatically assign a dedicated Setup Specialist to you. From 7e3625fac2465fc962604db789dd5bed9643d526 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:31:55 -0800 Subject: [PATCH 305/421] Update Deel.md to reflect "workspace" in lieu of "policy" --- docs/articles/expensify-classic/connections/Deel.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/articles/expensify-classic/connections/Deel.md b/docs/articles/expensify-classic/connections/Deel.md index 12e616d9657f..bdc4b89206ca 100644 --- a/docs/articles/expensify-classic/connections/Deel.md +++ b/docs/articles/expensify-classic/connections/Deel.md @@ -5,7 +5,7 @@ description: Automatically sync expenses from Expensify to Deel # Overview -This guide is for business clients who want to set up policies and synchronize expenses from Expensify to Deel. This one-way synchronization ensures that Expensify becomes the definitive source for all employee expenses. +This guide is for business clients who want to set up workspaces and synchronize expenses from Expensify to Deel. This one-way synchronization ensures that Expensify becomes the definitive source for all employee expenses. If you are a contractor or employee working for a company using Expensify, please refer to: @@ -16,7 +16,7 @@ If you are a contractor or employee working for a company using Expensify, pleas By integrating Expensify with Deel, you can utilize Expensify’s approval workflows to ensure timely payment through Deel for your team. -This process involves aligning user profiles and expense policies between Expensify and Deel. Once connected, Deel will scan for approved expenses from matched users included in selected workspaces for integration, allowing Deel to import these expenses for reimbursement. +This process involves aligning user profiles and expense workspaces between Expensify and Deel. Once connected, Deel will scan for approved expenses from matched users included in selected workspaces for integration, allowing Deel to import these expenses for reimbursement. This synchronization is one-way. Expenses and receipts logged and approved in Expensify will sync to Deel. Expenses logged in Deel will not sync to Expensify. @@ -27,7 +27,7 @@ This synchronization is one-way. Expenses and receipts logged and approved in Ex To establish a connection, make sure you have the following: - Deel Organization Manager permissions -- Expensify Admin permissions for policies you wish to integrate with Deel +- Expensify Admin permissions for workspaces you wish to integrate with Deel - A paid Expensify subscription to approve expenses and sync them to Deel Expensify Admin permissions can be intricate. Refer to [Expensify’s Introduction to Integration]([https://example.com](https://integrations.expensify.com/Integration-Server/doc/#introduction)) for more details. From 3f9c311ef0dbe594f3d41bc2f9a338a83479caad Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:32:46 -0800 Subject: [PATCH 306/421] Update Tax-Tracking.md to reflect "workspace" in lieu of "policy" --- docs/articles/expensify-classic/workspaces/Tax-Tracking.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md index 7b859c5101b1..c47e5ed51f32 100644 --- a/docs/articles/expensify-classic/workspaces/Tax-Tracking.md +++ b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md @@ -11,9 +11,9 @@ Expensify’s tax tracking feature allows you to: # How to Enable Tax Tracking Tax tracking can be enabled in the Tax section of the Workspace settings of any Workspace, whether group or individual. ## If Connected to an Accounting Integration -If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Policies > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. +If your group Workspace is connected to Xero, QuickBooks Online, Sage Intacct, or NetSuite, make sure to first enable tax via the connection configuration page (Settings > Workspaces > Group > [Workspace Name] > Connections > Configure) and then sync the connection. Your tax rates will be imported from the accounting system and indicated by its logo. ## Not Connected to an Accounting Integration -If your Workspace is not connected to an accounting system, go to Settings > Policies > Group > [Workspace Name] > Tax to enable tax. +If your Workspace is not connected to an accounting system, go to Settings > Workspaces > Group > [Workspace Name] > Tax to enable tax. # Tracking Tax by Expense Category To set a different tax rate for a specific expense type in the Workspace currency, go to Settings > Workspaces > Group > [Workspace Name] > Categories page. Click "Edit Rules" next to the desired category and set the "Category default tax". This will be applied to new expenses, overriding the default Workspace currency tax rate. From 3114a2e6d047490bee353186977a4d5968cf7ba9 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:33:18 -0800 Subject: [PATCH 307/421] Update Automatic-Receipt-Audit.md to reflect "workspace" in lieu of "policy" --- .../expensify-classic/reports/Automatic-Receipt-Audit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md b/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md index f0d112b86e9f..61640ce69b77 100644 --- a/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md +++ b/docs/articles/expensify-classic/reports/Automatic-Receipt-Audit.md @@ -17,5 +17,5 @@ All Expensify Control plans automatically come with Concierge Receipt Audit. If **Can I disable Concierge Receipt Audit?** -All Control plan policies automatically include Concierge Receipt Audit. At this time, it cannot be disabled. +All Control plan workspaces automatically include Concierge Receipt Audit. At this time, it cannot be disabled. {% include faq-end.md %} From c4d2cadb22a17161efa015e208f2aa6d19f8d773 Mon Sep 17 00:00:00 2001 From: greg-schroeder Date: Sun, 17 Nov 2024 22:33:54 -0800 Subject: [PATCH 308/421] Update Greenhouse.md to reflect "workspace" in lieu of "policy" --- docs/articles/expensify-classic/connections/Greenhouse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/connections/Greenhouse.md b/docs/articles/expensify-classic/connections/Greenhouse.md index b44e5a090d17..282ba33fd607 100644 --- a/docs/articles/expensify-classic/connections/Greenhouse.md +++ b/docs/articles/expensify-classic/connections/Greenhouse.md @@ -38,6 +38,6 @@ Expensify's direct integration with Greenhouse allows you to automatically send ## In Expensify: -1. Navigate to **Settings > Policies > Group > _[Workspace Name]_ > Members** +1. Navigate to **Settings > Workspaces > Group > _[Workspace Name]_ > Members** 2. The candidate you just sent to Expensify should be listed in the workspace members list 3. If the Recruiter (or Recruiting Coordinator) field was filled in in Greenhouse, the candidate will already be configured to submit reports to that recruiter for approval. If no Recruiter was selected, then the candidate will submit based on the Expensify workspace approval settings. From 51357853135e9190f97b3f0f621188c0f793e95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 09:55:04 +0100 Subject: [PATCH 309/421] use correct param --- src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx index 1922809bda2d..ccc2ed2ee45f 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.tsx @@ -33,7 +33,7 @@ function getTaxAmount(policy: OnyxEntry, transaction: OnyxEntry Date: Mon, 18 Nov 2024 09:57:09 +0100 Subject: [PATCH 310/421] cleanup default options --- src/components/Search/SearchFiltersChatsSelector.tsx | 2 -- src/components/Search/SearchFiltersParticipantsSelector.tsx | 2 -- src/components/Search/SearchRouter/SearchRouter.tsx | 2 +- src/pages/RoomInvitePage.tsx | 4 +--- src/pages/iou/request/MoneyRequestAttendeeSelector.tsx | 2 -- src/pages/iou/request/MoneyRequestParticipantsSelector.tsx | 4 ---- .../settings/AboutPage/ShareLogList/BaseShareLogList.tsx | 2 -- src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx | 2 -- src/pages/tasks/TaskAssigneeSelectorModal.tsx | 2 -- src/pages/tasks/TaskShareDestinationSelectorModal.tsx | 4 ---- src/pages/workspace/WorkspaceInvitePage.tsx | 4 ++-- 11 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 36b56867b99f..47cd2a6086d4 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -24,8 +24,6 @@ const defaultListOptions = { userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], headerMessage: '', }; diff --git a/src/components/Search/SearchFiltersParticipantsSelector.tsx b/src/components/Search/SearchFiltersParticipantsSelector.tsx index e7a60a5dc212..1e0c3ca8aae7 100644 --- a/src/components/Search/SearchFiltersParticipantsSelector.tsx +++ b/src/components/Search/SearchFiltersParticipantsSelector.tsx @@ -26,8 +26,6 @@ const defaultListOptions = { currentUserOption: null, headerMessage: '', categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; function getSelectedOptionData(option: Option): OptionData { diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index e65b12deb64b..4c57c0d1f63f 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -72,7 +72,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { const {options, areOptionsInitialized} = useOptionsList(); const searchOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: []}; } return OptionsListUtils.getSearchOptions(options, '', betas ?? []); }, [areOptionsInitialized, betas, options]); diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index c833fdb68ae6..3fafc163e5ff 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -72,7 +72,7 @@ function RoomInvitePage({ const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: []}; } const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], '', excludedUsers); @@ -96,8 +96,6 @@ function RoomInvitePage({ recentReports: [], currentUserOption: null, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails, selectedOptions]); diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx index 8732efd2e72d..ca655caea123 100644 --- a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx +++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx @@ -114,8 +114,6 @@ function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdde currentUserOption: null, headerMessage: '', categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; } const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, { diff --git a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx index 4478951555ef..a17cffd0cdd9 100644 --- a/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx +++ b/src/pages/iou/request/MoneyRequestParticipantsSelector.tsx @@ -103,8 +103,6 @@ function MoneyRequestParticipantsSelector({ currentUserOption: null, headerMessage: '', categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; } @@ -140,8 +138,6 @@ function MoneyRequestParticipantsSelector({ currentUserOption: null, headerMessage: '', categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; } diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 3f5db6cf5613..cd4674ce0165 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -35,8 +35,6 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], headerMessage: '', }; } diff --git a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx index a6ed5ca1b53e..6f54fc098633 100644 --- a/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx +++ b/src/pages/settings/Security/AddDelegate/AddDelegatePage.tsx @@ -51,8 +51,6 @@ function useOptions() { currentUserOption, headerMessage, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.tsx b/src/pages/tasks/TaskAssigneeSelectorModal.tsx index 59ebe08e41a4..33ca336206fe 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.tsx +++ b/src/pages/tasks/TaskAssigneeSelectorModal.tsx @@ -63,8 +63,6 @@ function useOptions() { currentUserOption, headerMessage, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], }; }, [optionsList.reports, optionsList.personalDetails, betas, isLoading]); diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index 8cd38a54f7f9..8465474ef609 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -64,8 +64,6 @@ function TaskShareDestinationSelectorModal() { userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], header: '', }; } @@ -78,8 +76,6 @@ function TaskShareDestinationSelectorModal() { userToInvite: null, currentUserOption: null, categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], header, }; }, [areOptionsInitialized, optionList.personalDetails, optionList.reports]); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3e63ae7cbe79..a259fc7b9ce1 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -87,12 +87,12 @@ function WorkspaceInvitePage({route, policy}: WorkspaceInvitePageProps) { const defaultOptions = useMemo(() => { if (!areOptionsInitialized) { - return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {recentReports: [], personalDetails: [], userToInvite: null, currentUserOption: null, categoryOptions: []}; } const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], '', excludedUsers, true); - return {...inviteOptions, recentReports: [], currentUserOption: null, categoryOptions: [], tagOptions: [], taxRatesOptions: []}; + return {...inviteOptions, recentReports: [], currentUserOption: null, categoryOptions: []}; }, [areOptionsInitialized, betas, excludedUsers, options.personalDetails]); const inviteOptions = useMemo( From 094c67aedf92ea3375e479825352542a533b8f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 10:05:43 +0100 Subject: [PATCH 311/421] fix previous migration --- src/components/TaxPicker.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/TaxPicker.tsx b/src/components/TaxPicker.tsx index 374c102b3e6c..4cb92eda3f10 100644 --- a/src/components/TaxPicker.tsx +++ b/src/components/TaxPicker.tsx @@ -7,7 +7,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import * as IOUUtils from '@libs/IOUUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TaxOptionsListUtils from '@libs/TaxOptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import type {IOUAction} from '@src/CONST'; @@ -48,13 +47,8 @@ function TaxPicker({selectedTaxRate = '', policyID, transactionID, insets, onSub const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); - const policy = PolicyUtils.getPolicy(policyID); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}` as `${typeof ONYXKEYS.COLLECTION.TRANSACTION}${string}`); - const [defaultTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); - const transaction = IOUUtils.shouldUseTransactionDraft(action) ? draftTransaction : defaultTransaction; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [transaction] = useOnyx( (() => { @@ -64,7 +58,6 @@ function TaxPicker({selectedTaxRate = '', policyID, transactionID, insets, onSub return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; })(), ); - const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); const isEditing = action === CONST.IOU.ACTION.EDIT; const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; From 4f2bb8b09e576d459ea0643418795098add1b427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Mon, 18 Nov 2024 10:09:16 +0100 Subject: [PATCH 312/421] use correct const --- src/libs/TaxOptionsListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TaxOptionsListUtils.ts b/src/libs/TaxOptionsListUtils.ts index 419cf31573b0..12c911d4e600 100644 --- a/src/libs/TaxOptionsListUtils.ts +++ b/src/libs/TaxOptionsListUtils.ts @@ -113,7 +113,7 @@ function getTaxRatesSection({ return policyRatesSections; } - if (numberOfTaxRates < CONST.TAX_RATES_LIST_THRESHOLD) { + if (numberOfTaxRates < CONST.STANDARD_LIST_ITEM_LIMIT) { policyRatesSections.push({ // "All" section when items amount less than the threshold title: '', From 495e9a35c93f5478fcb4ab39a9896aa4cf744a18 Mon Sep 17 00:00:00 2001 From: truph01 Date: Mon, 18 Nov 2024 16:33:18 +0700 Subject: [PATCH 313/421] fix: Add unit test --- tests/unit/OnboardingSelectorsTest.ts | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/unit/OnboardingSelectorsTest.ts diff --git a/tests/unit/OnboardingSelectorsTest.ts b/tests/unit/OnboardingSelectorsTest.ts new file mode 100644 index 000000000000..8a078416914a --- /dev/null +++ b/tests/unit/OnboardingSelectorsTest.ts @@ -0,0 +1,33 @@ +import type {OnyxValue} from 'react-native-onyx'; +import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +describe('onboardingSelectors', () => { + describe('hasCompletedGuidedSetupFlowSelector', () => { + it('Should return true if onboarding nvp is array ', async () => { + const onboarding = [] as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); + }); + it('Should return true if onboarding nvp is {}', async () => { + const onboarding = {} as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); + }); + it('Should return true if onboarding nvp contains only signupQualifier', async () => { + const onboarding = {signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB} as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); + }); + it('Should return true if onboarding nvp contains hasCompletedGuidedSetupFlow = true', async () => { + const onboarding = {hasCompletedGuidedSetupFlow: true} as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); + }); + it('Should return false if onboarding nvp contains hasCompletedGuidedSetupFlow = false', async () => { + const onboarding = {hasCompletedGuidedSetupFlow: false} as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(false); + }); + it('Should return true if onboarding nvp contains only selfTourViewed', async () => { + const onboarding = {selfTourViewed: true} as OnyxValue; + expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); + }); + }); +}); From 9091ec0b3a49f8a1f332b5441e6441ee44abdafa Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Nov 2024 10:34:51 +0100 Subject: [PATCH 314/421] fix lint --- src/pages/ReportAvatar.tsx | 2 +- src/types/utils/whitelistedReportKeys.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx index cb3fc0cda333..649f690cb1c6 100644 --- a/src/pages/ReportAvatar.tsx +++ b/src/pages/ReportAvatar.tsx @@ -37,7 +37,7 @@ function ReportAvatar({route}: ReportAvatarProps) { originalFileName: policy?.originalFileName ?? policy?.id ?? report?.policyID ?? '', isWorkspaceAvatar: true, }; - }, [report, policy]); + }, [report, policy, reportMetadata]); return ( Date: Mon, 18 Nov 2024 16:36:18 +0700 Subject: [PATCH 315/421] fix: lint --- tests/unit/OnboardingSelectorsTest.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/OnboardingSelectorsTest.ts b/tests/unit/OnboardingSelectorsTest.ts index 8a078416914a..2f81f7a8071d 100644 --- a/tests/unit/OnboardingSelectorsTest.ts +++ b/tests/unit/OnboardingSelectorsTest.ts @@ -1,31 +1,31 @@ import type {OnyxValue} from 'react-native-onyx'; import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import type ONYXKEYS from '@src/ONYXKEYS'; describe('onboardingSelectors', () => { describe('hasCompletedGuidedSetupFlowSelector', () => { - it('Should return true if onboarding nvp is array ', async () => { + it('Should return true if onboarding nvp is array ', () => { const onboarding = [] as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); }); - it('Should return true if onboarding nvp is {}', async () => { + it('Should return true if onboarding nvp is {}', () => { const onboarding = {} as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); }); - it('Should return true if onboarding nvp contains only signupQualifier', async () => { + it('Should return true if onboarding nvp contains only signupQualifier', () => { const onboarding = {signupQualifier: CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB} as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); }); - it('Should return true if onboarding nvp contains hasCompletedGuidedSetupFlow = true', async () => { + it('Should return true if onboarding nvp contains hasCompletedGuidedSetupFlow = true', () => { const onboarding = {hasCompletedGuidedSetupFlow: true} as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); }); - it('Should return false if onboarding nvp contains hasCompletedGuidedSetupFlow = false', async () => { + it('Should return false if onboarding nvp contains hasCompletedGuidedSetupFlow = false', () => { const onboarding = {hasCompletedGuidedSetupFlow: false} as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(false); }); - it('Should return true if onboarding nvp contains only selfTourViewed', async () => { + it('Should return true if onboarding nvp contains only selfTourViewed', () => { const onboarding = {selfTourViewed: true} as OnyxValue; expect(hasCompletedGuidedSetupFlowSelector(onboarding)).toBe(true); }); From 9bcec51d49d45b52a3845642f6056205fc3aefb0 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 18 Nov 2024 10:52:31 +0100 Subject: [PATCH 316/421] Minor improvements --- src/libs/CardUtils.ts | 14 +++++++++----- tests/unit/CardUtilsTest.ts | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index dcf2926460cd..029906ce2a3a 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -135,22 +135,26 @@ function maskCard(lastFour = ''): string { * Converts given 'X' to '•' for the entire card string. * * @param cardName - card name with XXXX in the middle. - * @param bank - card bank. + * @param feed - card feed. * @returns - The masked card string. */ -function maskCardNumber(cardName: string, bank: string | undefined): string { - if (!cardName || cardName === '' || !bank) { +function maskCardNumber(cardName: string, feed: string | undefined): string { + if (!cardName || cardName === '' || !feed) { return ''; } const hasSpace = /\s/.test(cardName); const maskedString = cardName.replace(/X/g, '•'); - const isAmexBank = [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_DIRECT].some((value) => value === bank); + const isAmexBank = [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_DIRECT].some((value) => value === feed); + + if (hasSpace) { + return cardName; + } if (isAmexBank && maskedString.length === 15) { return maskedString.replace(/(.{4})(.{6})(.{5})/, '$1 $2 $3'); } - return hasSpace ? cardName : maskedString.replace(/(.{4})/g, '$1 ').trim(); + return maskedString.replace(/(.{4})/g, '$1 ').trim(); } /** diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 1917833df710..58dda1f3566f 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -223,25 +223,25 @@ describe('CardUtils', () => { expect(maskedCardNumber).toBe('4808 01•• •••• 2554'); }); - it("Should return card number without changes if it's provided in the 'CREDIT CARD...6607' format", () => { + it('Should return card number without changes if it has empty space', () => { const cardNumber = 'CREDIT CARD...6607'; const maskedCardNumber = CardUtils.maskCardNumber(cardNumber, CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE); expect(maskedCardNumber).toBe(cardNumber); }); - it("Should return the Amex direct feed card number divided into 4/6/5 chunks, with 'X' replaced by '•'", () => { + it("Should return the Amex direct feed card number divided into 4/6/5 chunks, with 'X' replaced by '•' if it's provided in '211944XXXXX6557' format", () => { const cardNumber = '211944XXXXX6557'; const maskedCardNumber = CardUtils.maskCardNumber(cardNumber, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX_DIRECT); expect(maskedCardNumber).toBe('2119 44•••• •6557'); }); - it("Should return the Amex custom feed card number divided into 4/6/5 chunks, with 'X' replaced by '•'", () => { + it("Should return the Amex custom feed card number divided into 4/6/5 chunks, with 'X' replaced by '•' if it's provided in '211944XXXXX6557' format", () => { const cardNumber = '211944XXXXX6557'; const maskedCardNumber = CardUtils.maskCardNumber(cardNumber, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX); expect(maskedCardNumber).toBe('2119 44•••• •6557'); }); - it('Should return empty string if undefined bank was provided', () => { + it('Should return empty string if undefined feed was provided', () => { const cardNumber = '480801XXXXXX2554'; const maskedCardNumber = CardUtils.maskCardNumber(cardNumber, undefined); expect(maskedCardNumber).toBe(''); From a4d46242d556ae7e139810717b8a928eecafb6ea Mon Sep 17 00:00:00 2001 From: burczu Date: Fri, 15 Nov 2024 12:01:14 +0100 Subject: [PATCH 317/421] agreements step implementation --- src/languages/en.ts | 7 ++ src/languages/es.ts | 13 ++- .../Agreements/{Agreements.tsx => index.tsx} | 8 +- .../Agreements/substeps/Confirmation.tsx | 93 ++++++++++++++++++- .../ReimbursementAccountPage.tsx | 2 +- 5 files changed, 114 insertions(+), 9 deletions(-) rename src/pages/ReimbursementAccount/NonUSD/Agreements/{Agreements.tsx => index.tsx} (89%) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1a703f1bea1b..0b304537627a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2330,7 +2330,14 @@ const translations = { agreementsStep: { agreements: 'Agreements', pleaseConfirm: 'Please confirm the agreements below', + iAmAuthorized: 'I am authorized to use the business bank account for business spend.', + iCertify: 'I certify that the information provided is true and accurate.', + termsAndConditions: 'terms and conditions.', accept: 'Accept and add bank account', + error: { + authorized: 'You must be a controlling officer with authorization to operate the business bank account', + certify: 'Please certify that the information is true and accurate', + }, }, finishStep: { connect: 'Connect bank account', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2bb66cec6548..c570a0e2742e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2353,9 +2353,16 @@ const translations = { weAreWaiting: 'Estamos esperando que otros verifiquen sus identidades como directores o altos funcionarios de la empresa.', }, agreementsStep: { - agreements: 'Acuerdos', - pleaseConfirm: 'Por favor confirme los acuerdos a continuación', - accept: 'Aceptar y añadir cuenta bancaria', + agreements: 'Agreements', + pleaseConfirm: 'Please confirm the agreements below', + iAmAuthorized: 'I am authorized to use the business bank account for business spend.', + iCertify: 'I certify that the information provided is true and accurate.', + termsAndConditions: 'terms and conditions.', + accept: 'Accept and add bank account', + error: { + authorized: 'You must be a controlling officer with authorization to operate the business bank account', + certify: 'Please certify that the information is true and accurate', + }, }, finishStep: { connect: 'Conectar cuenta bancaria', diff --git a/src/pages/ReimbursementAccount/NonUSD/Agreements/Agreements.tsx b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx similarity index 89% rename from src/pages/ReimbursementAccount/NonUSD/Agreements/Agreements.tsx rename to src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx index 605157e2fe33..3c79cc54fdaf 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Agreements/Agreements.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx @@ -17,7 +17,7 @@ type AgreementsProps = { const bodyContent: Array> = [Confirmation]; -function Agreements({onBackButtonPress, onSubmit}: AgreementsProps) { +function Index({onBackButtonPress, onSubmit}: AgreementsProps) { const {translate} = useLocalize(); const submit = () => { @@ -41,7 +41,7 @@ function Agreements({onBackButtonPress, onSubmit}: AgreementsProps) { return ( {translate('agreementsStep.iAmAuthorized')}
; +} + +function CertifyTrueAndAccurateLabel() { + const {translate} = useLocalize(); + return {translate('agreementsStep.iCertify')}; +} + +function TermsAndConditionsLabel() { + const {translate} = useLocalize(); + return ( + + {translate('common.iAcceptThe')} + {`${translate('agreementsStep.termsAndConditions')}`} + + ); +} function Confirmation({onNext}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const defaultValues = { + [AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT]: + !!reimbursementAccount?.achData?.additionalData?.corpay?.[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT] ?? reimbursementAccountDraft?.[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT] ?? '', + [PROVIDE_TRUTHFUL_INFORMATION]: + !!reimbursementAccount?.achData?.additionalData?.corpay?.[PROVIDE_TRUTHFUL_INFORMATION] ?? reimbursementAccountDraft?.[PROVIDE_TRUTHFUL_INFORMATION] ?? '', + [AGREE_TO_TERMS_AND_CONDITIONS]: + !!reimbursementAccount?.achData?.additionalData?.corpay?.[AGREE_TO_TERMS_AND_CONDITIONS] ?? reimbursementAccountDraft?.[AGREE_TO_TERMS_AND_CONDITIONS] ?? '', + }; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (!ValidationUtils.isRequiredFulfilled(values[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT])) { + errors[AUTHORIZED_TO_BIND_CLIENT_TO_AGREEMENT] = translate('agreementsStep.error.authorized'); + } + + if (!ValidationUtils.isRequiredFulfilled(values[PROVIDE_TRUTHFUL_INFORMATION])) { + errors[PROVIDE_TRUTHFUL_INFORMATION] = translate('agreementsStep.error.certify'); + } + + if (!ValidationUtils.isRequiredFulfilled(values[AGREE_TO_TERMS_AND_CONDITIONS])) { + errors[AGREE_TO_TERMS_AND_CONDITIONS] = translate('common.error.acceptTerms'); + } + + return errors; + }, + [translate], + ); + return ( {translate('agreementsStep.pleaseConfirm')} + + + ); } diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 6c3b289e59df..d4c961255b45 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -40,7 +40,7 @@ import CompanyStep from './CompanyStep'; import ConnectBankAccount from './ConnectBankAccount/ConnectBankAccount'; import ContinueBankAccountSetup from './ContinueBankAccountSetup'; import EnableBankAccount from './EnableBankAccount/EnableBankAccount'; -import Agreements from './NonUSD/Agreements/Agreements'; +import Agreements from './NonUSD/Agreements'; import BankInfo from './NonUSD/BankInfo/BankInfo'; import BeneficialOwnerInfo from './NonUSD/BeneficialOwnerInfo/BeneficialOwnerInfo'; import BusinessInfo from './NonUSD/BusinessInfo/BusinessInfo'; From c975b616890fd30c0a04b131fef74cffd668e064 Mon Sep 17 00:00:00 2001 From: burczu Date: Fri, 15 Nov 2024 12:04:07 +0100 Subject: [PATCH 318/421] components names fixed --- .../ReimbursementAccount/NonUSD/Agreements/index.tsx | 8 ++++---- .../NonUSD/Finish/{Finish.tsx => index.tsx} | 0 .../ReimbursementAccount/ReimbursementAccountPage.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/pages/ReimbursementAccount/NonUSD/Finish/{Finish.tsx => index.tsx} (100%) diff --git a/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx index 3c79cc54fdaf..605157e2fe33 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Agreements/index.tsx @@ -17,7 +17,7 @@ type AgreementsProps = { const bodyContent: Array> = [Confirmation]; -function Index({onBackButtonPress, onSubmit}: AgreementsProps) { +function Agreements({onBackButtonPress, onSubmit}: AgreementsProps) { const {translate} = useLocalize(); const submit = () => { @@ -41,7 +41,7 @@ function Index({onBackButtonPress, onSubmit}: AgreementsProps) { return ( Date: Fri, 15 Nov 2024 12:07:39 +0100 Subject: [PATCH 319/421] finish step implementation --- src/languages/en.ts | 7 +++ src/languages/es.ts | 9 ++- .../NonUSD/Finish/index.tsx | 55 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 0b304537627a..b046116b74a5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2341,6 +2341,13 @@ const translations = { }, finishStep: { connect: 'Connect bank account', + letsFinish: "Let's finish in chat!", + thanksFor: + "Thanks for those details. A dedicated support agent will now review your information. We'll circle back if we need anything else from you, but in the meantime, you can chat with us any time if you have questions.", + iHaveA: 'I have a question', + enable2FA: 'Enable two-factor authentication (2FA) to prevent fraud', + weTake: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', + secure: 'Secure your account', }, reimbursementAccountLoadingAnimation: { oneMoment: 'One moment', diff --git a/src/languages/es.ts b/src/languages/es.ts index c570a0e2742e..c5966da57c74 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2365,7 +2365,14 @@ const translations = { }, }, finishStep: { - connect: 'Conectar cuenta bancaria', + connect: 'Connect bank account', + letsFinish: "Let's finish in chat!", + thanksFor: + "Thanks for those details. A dedicated support agent will now review your information. We'll circle back if we need anything else from you, but in the meantime, you can chat with us any time if you have questions.", + iHaveA: 'I have a question', + enable2FA: 'Enable two-factor authentication (2FA) to prevent fraud', + weTake: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', + secure: 'Secure your account', }, reimbursementAccountLoadingAnimation: { oneMoment: 'Un momento', diff --git a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx index 69c0e9e77a45..dd6438335d79 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx @@ -1,18 +1,32 @@ import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; +import * as Report from '@userActions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; function Finish() { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const handleBackButtonPress = () => { Navigation.goBack(); }; + const handleNavigateToConciergeChat = () => Report.navigateToConciergeChat(true); return ( - + +
+ {translate('finishStep.thanksFor')} +
+
{ + Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID))); + }, + icon: Expensicons.Shield, + shouldShowRightIcon: true, + iconRight: Expensicons.NewWindow, + wrapperStyle: [styles.cardMenuItem], + }, + ]} + > + + {translate('finishStep.weTake')} + +
+
); } From 0657ca5fedda9ffd2bba7359487450600747ba55 Mon Sep 17 00:00:00 2001 From: burczu Date: Mon, 18 Nov 2024 11:04:24 +0100 Subject: [PATCH 320/421] translations applied --- src/languages/en.ts | 2 +- src/languages/es.ts | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b046116b74a5..c43b6ed9a478 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2343,7 +2343,7 @@ const translations = { connect: 'Connect bank account', letsFinish: "Let's finish in chat!", thanksFor: - "Thanks for those details. A dedicated support agent will now review your information. We'll circle back if we need anything else from you, but in the meantime, you can chat with us any time if you have questions.", + "Thanks for those details. A dedicated support agent will now review your information. We'll circle back if we need anything else from you, but in the meantime, feel free to reach out to us with any questions.", iHaveA: 'I have a question', enable2FA: 'Enable two-factor authentication (2FA) to prevent fraud', weTake: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', diff --git a/src/languages/es.ts b/src/languages/es.ts index c5966da57c74..1928ebf1be2c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2341,7 +2341,7 @@ const translations = { uploadID: 'Subir documento de identidad y prueba de domicilio', id: 'Identificación (licencia de conducir o pasaporte)', personalAddress: 'Prueba de domicilio personal (por ejemplo, factura de servicios públicos)', - letsDoubleCheck: 'Vamos a comprobar que todo está bien.', + letsDoubleCheck: 'Vamos a verificar que todo esté correcto.', legalName: 'Nombre legal', proofOf: 'Comprobante de domicilio personal', enterOneEmail: 'Introduce el correo electrónico del director o alto funcionario en', @@ -2353,26 +2353,26 @@ const translations = { weAreWaiting: 'Estamos esperando que otros verifiquen sus identidades como directores o altos funcionarios de la empresa.', }, agreementsStep: { - agreements: 'Agreements', - pleaseConfirm: 'Please confirm the agreements below', - iAmAuthorized: 'I am authorized to use the business bank account for business spend.', - iCertify: 'I certify that the information provided is true and accurate.', - termsAndConditions: 'terms and conditions.', - accept: 'Accept and add bank account', + agreements: 'Acuerdos', + pleaseConfirm: 'Por favor confirme los acuerdos a continuación', + iAmAuthorized: 'Estoy autorizado para usar la cuenta bancaria para gastos del negocio.', + iCertify: 'Certifico que la información proporcionada es verdadera y correcta.', + termsAndConditions: 'términos y condiciones.', + accept: 'Agregar y aceptar cuenta bancaria', error: { - authorized: 'You must be a controlling officer with authorization to operate the business bank account', - certify: 'Please certify that the information is true and accurate', + authorized: 'Debe ser un funcionario controlador con autorización para operar la cuenta bancaria comercial', + certify: 'Por favor certifique que la información es verdadera y exacta', }, }, finishStep: { - connect: 'Connect bank account', - letsFinish: "Let's finish in chat!", + connect: 'Conectar cuenta bancaria', + letsFinish: '¡Terminemos en el chat!', thanksFor: - "Thanks for those details. A dedicated support agent will now review your information. We'll circle back if we need anything else from you, but in the meantime, you can chat with us any time if you have questions.", - iHaveA: 'I have a question', - enable2FA: 'Enable two-factor authentication (2FA) to prevent fraud', - weTake: 'We take your security seriously. Please set up 2FA now to add an extra layer of protection to your account.', - secure: 'Secure your account', + 'Gracias por esos detalles. Un agente de soporte dedicado revisará ahora tu información. Nos pondremos en contacto si necesitamos algo más de tu parte, pero mientras tanto, no dudes en comunicarte con nosotros si tienes alguna pregunta.', + iHaveA: 'Tengo una pregunta', + enable2FA: 'Habilite la autenticación de dos factores (2FA) para prevenir fraudes', + weTake: 'Nos tomamos su seguridad en serio. Por favor, configure 2FA ahora para agregar una capa adicional de protección a su cuenta.', + secure: 'Asegure su cuenta', }, reimbursementAccountLoadingAnimation: { oneMoment: 'Un momento', From 7550a55faf455175c66995b6cab0e9359b801f78 Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 18 Nov 2024 17:14:01 +0700 Subject: [PATCH 321/421] fix typo --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 466eb39bf4bd..1068b41f4026 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -728,7 +728,7 @@ function validateSecondaryLogin(loginList: OnyxEntry, contactMethod: value: {isLoading: false}, }, ]; - // Sometimes we will also need to reset the validateCodeSent of ONYXKEYS.VALIDATE_ACTION_CODE in order to receive the magic next time we open the ValidateActionCodeModal. + // Sometimes we will also need to reset the validateCodeSent of ONYXKEYS.VALIDATE_ACTION_CODE in order to receive the magic next time we open the ValidateCodeActionModal. if (shouldResetActionCode) { const optimisticResetActionCode = { onyxMethod: Onyx.METHOD.MERGE, From f218f1e47a4fb3ca56f5061e111318527f882221 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Mon, 18 Nov 2024 11:21:38 +0100 Subject: [PATCH 322/421] fix: cr fixes --- .../substeps/BeneficialOwnerCheckUBO.tsx | 34 ---------- .../BeneficialOwnersStep.tsx | 16 +++-- .../BeneficialOwnerCheck.tsx | 65 ------------------- .../BeneficialOwnerInfo.tsx | 11 ++-- .../BeneficialOwnersList.tsx | 6 +- .../NonUSD/SignerInfo/DirectorCheck.tsx | 31 --------- .../NonUSD/SignerInfo/index.tsx | 5 +- 7 files changed, 26 insertions(+), 142 deletions(-) delete mode 100644 src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx delete mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx delete mode 100644 src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx deleted file mode 100644 index 478642416e30..000000000000 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import YesNoStep from '@components/SubStepForms/YesNoStep'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type BeneficialOwnerCheckUBOProps = { - /** The title of the question */ - title: string; - - /** The default value of the radio button */ - defaultValue: boolean; - - /** Callback when the value is selected */ - onSelectedValue: (value: boolean) => void; -}; - -function BeneficialOwnerCheckUBO({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckUBOProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - - return ( - - ); -} - -BeneficialOwnerCheckUBO.displayName = 'BeneficialOwnerCheckUBO'; - -export default BeneficialOwnerCheckUBO; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx index 6d70e966fa2d..6fd3bcb09f80 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx @@ -2,14 +2,15 @@ import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import BeneficialOwnerCheckUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO'; import AddressUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO'; import ConfirmationUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO'; import DateOfBirthUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO'; @@ -30,6 +31,7 @@ const bodyContent: Array> = [Le function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { const {translate} = useLocalize(); + const styles = useThemeStyles(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); @@ -214,16 +216,20 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { stepNames={CONST.BANK_ACCOUNT.STEP_NAMES} > {currentUBOSubstep === SUBSTEP.IS_USER_UBO && ( - )} {currentUBOSubstep === SUBSTEP.IS_ANYONE_ELSE_UBO && ( - @@ -240,8 +246,10 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { )} {currentUBOSubstep === SUBSTEP.ARE_THERE_MORE_UBOS && ( - diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx deleted file mode 100644 index 4d108de6dae1..000000000000 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, {useMemo, useState} from 'react'; -import FormProvider from '@components/Form/FormProvider'; -import type {Choice} from '@components/RadioButtons'; -import RadioButtons from '@components/RadioButtons'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import ONYXKEYS from '@src/ONYXKEYS'; - -type BeneficialOwnerCheckProps = { - /** The title of the question */ - title: string; - - /** The default value of the radio button */ - defaultValue: boolean; - - /** Callback when the value is selected */ - onSelectedValue: (value: boolean) => void; -}; - -function BeneficialOwnerCheck({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const [value, setValue] = useState(defaultValue); - - const handleSubmit = () => { - onSelectedValue(value); - }; - const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); - const options = useMemo( - () => [ - { - label: translate('common.yes'), - value: 'true', - }, - { - label: translate('common.no'), - value: 'false', - }, - ], - [translate], - ); - - return ( - - {title} - {translate('ownershipInfoStep.regulationsRequire')} - - - ); -} - -BeneficialOwnerCheck.displayName = 'BeneficialOwnerCheck'; - -export default BeneficialOwnerCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerInfo.tsx index 529573e9eb82..49ad1de6b56f 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerInfo.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerInfo.tsx @@ -3,6 +3,7 @@ import type {ComponentType} from 'react'; import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; @@ -10,7 +11,6 @@ import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import BeneficialOwnerCheck from './BeneficialOwnerCheck'; import Address from './BeneficialOwnerDetailsFormSubSteps/Address'; import Confirmation from './BeneficialOwnerDetailsFormSubSteps/Confirmation'; import DateOfBirth from './BeneficialOwnerDetailsFormSubSteps/DateOfBirth'; @@ -300,16 +300,18 @@ function BeneficialOwnerInfo({onBackButtonPress, onSubmit}: BeneficialOwnerInfoP startStepIndex={3} > {currentSubStep === SUBSTEP.IS_USER_BENEFICIAL_OWNER && ( - )} {currentSubStep === SUBSTEP.IS_ANYONE_ELSE_BENEFICIAL_OWNER && ( - @@ -329,8 +331,9 @@ function BeneficialOwnerInfo({onBackButtonPress, onSubmit}: BeneficialOwnerInfoP )} {currentSubStep === SUBSTEP.ARE_THERE_MORE_BENEFICIAL_OWNERS && ( - diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx index e3351a5d0a62..2b6853472f7f 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx @@ -70,6 +70,8 @@ function BeneficialOwnersList({handleConfirmation, ownerKeys, handleOwnerEdit, h ); }); + const areThereOwners = owners !== undefined && owners?.length > 0; + return ( {({safeAreaPaddingBottomStyle}) => ( @@ -79,7 +81,7 @@ function BeneficialOwnersList({handleConfirmation, ownerKeys, handleOwnerEdit, h > {translate('beneficialOwnerInfoStep.letsDoubleCheck')} {translate('beneficialOwnerInfoStep.regulationRequiresUsToVerifyTheIdentity')} - {owners !== undefined && owners?.length > 0 && ( + {areThereOwners && ( {`${translate('beneficialOwnerInfoStep.owners')}:`} {owners} @@ -91,7 +93,7 @@ function BeneficialOwnersList({handleConfirmation, ownerKeys, handleOwnerEdit, h title={ownershipChartValue.map((file) => file.name).join(', ') || ''} shouldShowRightIcon onPress={handleOwnershipChartEdit} - style={[styles.mt8]} + style={[areThereOwners ? styles.mt8 : styles.mt0]} /> )} diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx deleted file mode 100644 index 7535f72a3970..000000000000 --- a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/DirectorCheck.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import YesNoStep from '@components/SubStepForms/YesNoStep'; -import useLocalize from '@hooks/useLocalize'; - -type DirectorCheckProps = { - /** The title of the question */ - title: string; - - /** The default value of the radio button */ - defaultValue: boolean; - - /** Callback when the value is selected */ - onSelectedValue: (value: boolean) => void; -}; - -function DirectorCheck({title, onSelectedValue, defaultValue}: DirectorCheckProps) { - const {translate} = useLocalize(); - - return ( - - ); -} - -DirectorCheck.displayName = 'DirectorCheck'; - -export default DirectorCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx index 363e444c5641..1ba513c07807 100644 --- a/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/SignerInfo/index.tsx @@ -2,6 +2,7 @@ import type {ComponentType} from 'react'; import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; @@ -9,7 +10,6 @@ import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import DirectorCheck from './DirectorCheck'; import EnterEmail from './EnterEmail'; import HangTight from './HangTight'; import Confirmation from './substeps/Confirmation'; @@ -119,8 +119,9 @@ function SignerInfo({onBackButtonPress, onSubmit}: SignerInfoProps) { startStepIndex={4} > {currentSubStep === SUBSTEP.IS_DIRECTOR && ( - From 2ff38ff66500298d6e5dbb883a6e56e9ed6377a5 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Mon, 18 Nov 2024 11:27:35 +0100 Subject: [PATCH 323/421] move isLoadingPrivateNotes key to reportMetadata_ --- src/libs/DebugUtils.ts | 1 - src/libs/actions/Report.ts | 6 +++--- src/pages/ReportDetailsPage.tsx | 4 ++-- .../home/report/withReportAndPrivateNotesOrNotFound.tsx | 6 +++--- src/types/form/DebugReportForm.ts | 2 -- src/types/onyx/Report.ts | 3 --- src/types/onyx/ReportMetadata.ts | 3 +++ src/types/utils/whitelistedReportKeys.ts | 1 - 8 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 2f0da08b194d..98dbbd4014a1 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -65,7 +65,6 @@ const REPORT_BOOLEAN_PROPERTIES: Array = [ 'isWaitingOnBankAccount', 'isCancelledIOU', 'isHidden', - 'isLoadingPrivateNotes', ] satisfies Array; const REPORT_DATE_PROPERTIES: Array = ['lastVisibleActionCreated', 'lastReadTime', 'lastMentionedTime', 'lastVisibleActionLastModified'] satisfies Array; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 32c0a40876d7..d5d6aee59a16 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3440,7 +3440,7 @@ function getReportPrivateNote(reportID: string | undefined) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingPrivateNotes: true, }, @@ -3450,7 +3450,7 @@ function getReportPrivateNote(reportID: string | undefined) { const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingPrivateNotes: false, }, @@ -3460,7 +3460,7 @@ function getReportPrivateNote(reportID: string | undefined) { const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, value: { isLoadingPrivateNotes: false, }, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 9e438f0549e2..19fd481e508f 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -80,7 +80,7 @@ const CASES = { type CaseID = ValueOf; -function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { +function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDetailsPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -183,7 +183,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { // 1. HeaderView return CASES.DEFAULT; }, [isInvoiceReport, isMoneyRequestReport, isSingleTransactionView]); - const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined; + const isPrivateNotesFetchTriggered = reportMetadata?.isLoadingPrivateNotes !== undefined; const requestParentReportAction = useMemo(() => { // 2. MoneyReport case diff --git a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.tsx b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.tsx index 2fed275045bc..b27a2a2c9fb9 100644 --- a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.tsx +++ b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.tsx @@ -31,14 +31,14 @@ export default function (pageTitle: TranslationPaths) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const [session] = useOnyx(ONYXKEYS.SESSION); - const {route, report} = props; + const {route, report, reportMetadata} = props; const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`); const accountID = ('accountID' in route.params && route.params.accountID) || ''; - const isPrivateNotesFetchTriggered = report?.isLoadingPrivateNotes !== undefined; + const isPrivateNotesFetchTriggered = reportMetadata?.isLoadingPrivateNotes !== undefined; const prevIsOffline = usePrevious(isOffline); const isReconnecting = prevIsOffline && !isOffline; const isOtherUserNote = !!accountID && Number(session?.accountID) !== Number(accountID); - const isPrivateNotesFetchFinished = isPrivateNotesFetchTriggered && !report.isLoadingPrivateNotes; + const isPrivateNotesFetchFinished = isPrivateNotesFetchTriggered && !reportMetadata.isLoadingPrivateNotes; const isPrivateNotesUndefined = accountID ? report?.privateNotes?.[Number(accountID)]?.note === undefined : isEmptyObject(report?.privateNotes); useEffect(() => { diff --git a/src/types/form/DebugReportForm.ts b/src/types/form/DebugReportForm.ts index c21ca6f2dc73..87f4f38ee329 100644 --- a/src/types/form/DebugReportForm.ts +++ b/src/types/form/DebugReportForm.ts @@ -11,7 +11,6 @@ const INPUT_IDS = { HAS_OUTSTANDING_CHILD_REQUEST: 'hasOutstandingChildRequest', HAS_OUTSTANDING_CHILD_TASK: 'hasOutstandingChildTask', IS_CANCELLED_IOU: 'isCancelledIOU', - IS_LOADING_PRIVATE_NOTES: 'isLoadingPrivateNotes', IS_OWN_POLICY_EXPENSE_CHAT: 'isOwnPolicyExpenseChat', IS_PINNED: 'isPinned', IS_WAITING_ON_BANK_ACCOUNT: 'isWaitingOnBankAccount', @@ -60,7 +59,6 @@ type DebugReportForm = Form< [INPUT_IDS.HAS_OUTSTANDING_CHILD_REQUEST]: boolean; [INPUT_IDS.HAS_OUTSTANDING_CHILD_TASK]: boolean; [INPUT_IDS.IS_CANCELLED_IOU]: boolean; - [INPUT_IDS.IS_LOADING_PRIVATE_NOTES]: boolean; [INPUT_IDS.IS_OWN_POLICY_EXPENSE_CHAT]: boolean; [INPUT_IDS.IS_PINNED]: boolean; [INPUT_IDS.IS_WAITING_ON_BANK_ACCOUNT]: boolean; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 1241556c8dbd..c2d8f7b21428 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -232,9 +232,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Collection of participant private notes, indexed by their accountID */ privateNotes?: Record; - /** Whether participants private notes are being currently loaded */ - isLoadingPrivateNotes?: boolean; - /** Pending members of the report */ pendingChatMembers?: PendingChatMember[]; diff --git a/src/types/onyx/ReportMetadata.ts b/src/types/onyx/ReportMetadata.ts index 65a6f153c51b..36c5aee1cb57 100644 --- a/src/types/onyx/ReportMetadata.ts +++ b/src/types/onyx/ReportMetadata.ts @@ -17,6 +17,9 @@ type ReportMetadata = { /** The time when user last visited the report */ lastVisitTime?: string; + + /** Whether participants private notes are being currently loaded */ + isLoadingPrivateNotes?: boolean; }; export default ReportMetadata; diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts index 3c566c987526..07173c8bc073 100644 --- a/src/types/utils/whitelistedReportKeys.ts +++ b/src/types/utils/whitelistedReportKeys.ts @@ -61,7 +61,6 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback< nonReimbursableTotal: unknown; isHidden: unknown; privateNotes: unknown; - isLoadingPrivateNotes: unknown; pendingChatMembers: unknown; transactionThreadReportID: unknown; fieldList: unknown; From 1836991c5cf891cba01977ad631b8e5617d498c7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 18 Nov 2024 12:28:52 +0100 Subject: [PATCH 324/421] Remove check for feed --- src/libs/CardUtils.ts | 2 +- tests/unit/CardUtilsTest.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 029906ce2a3a..77aeb8e0ecc3 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -139,7 +139,7 @@ function maskCard(lastFour = ''): string { * @returns - The masked card string. */ function maskCardNumber(cardName: string, feed: string | undefined): string { - if (!cardName || cardName === '' || !feed) { + if (!cardName || cardName === '') { return ''; } const hasSpace = /\s/.test(cardName); diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 58dda1f3566f..9d4af5aa3760 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -241,10 +241,10 @@ describe('CardUtils', () => { expect(maskedCardNumber).toBe('2119 44•••• •6557'); }); - it('Should return empty string if undefined feed was provided', () => { + it('Should return masked card number even if undefined feed was provided', () => { const cardNumber = '480801XXXXXX2554'; const maskedCardNumber = CardUtils.maskCardNumber(cardNumber, undefined); - expect(maskedCardNumber).toBe(''); + expect(maskedCardNumber).toBe('4808 01•• •••• 2554'); }); it('Should return empty string if invalid card name was provided', () => { From 697fd4585973bb477d06ab273a865628ba84bf78 Mon Sep 17 00:00:00 2001 From: Tom Rhys Jones Date: Mon, 18 Nov 2024 11:33:04 +0000 Subject: [PATCH 325/421] casing fix.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 728954acd7f7..726b2dc186f1 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -134,7 +134,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { '\n' + 'Here’s how to set up your bank account:\n' + '\n' + - '1. Click the Settings tab.\n' + + '1. Click the settings tab.\n' + '2. Click *Wallet* > *Bank accounts* > *+ Add bank account*.\n' + '3. Connect your bank account.\n' + '\n' + From 2b38733469dc2441821c3317b1cbed04713d1c13 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 18 Nov 2024 12:45:06 +0100 Subject: [PATCH 326/421] Call reconnectApp instead of openApp when ND is reopened in hybrid --- src/libs/actions/Session/index.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index eda761b9637b..a867bfcd7e36 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -528,7 +528,14 @@ function signInAfterTransitionFromOldDot(transitionURL: string) { [ONYXKEYS.NVP_TRYNEWDOT]: {classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'}}, }), ) - .then(App.openApp) + .then(() => { + if (clearOnyxOnStart === 'true') { + // We clear Onyx when this flag is set to true so we have to download all data + App.openApp(); + } else { + App.reconnectApp(); + } + }) .catch((error) => { Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error}); }) From 7d0a6a1c4408cf1234bd0c3965e47830a1ffbf53 Mon Sep 17 00:00:00 2001 From: burczu Date: Mon, 18 Nov 2024 12:48:55 +0100 Subject: [PATCH 327/421] missing descriptions added --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../NonUSD/Agreements/substeps/Confirmation.tsx | 1 + src/pages/ReimbursementAccount/NonUSD/SignerInfo/EnterEmail.tsx | 1 + 4 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index c43b6ed9a478..32ceba108ab9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2330,6 +2330,7 @@ const translations = { agreementsStep: { agreements: 'Agreements', pleaseConfirm: 'Please confirm the agreements below', + regulationRequiresUs: 'Regulation requires us to verify the identity of any individual who owns more than 25% of the business.', iAmAuthorized: 'I am authorized to use the business bank account for business spend.', iCertify: 'I certify that the information provided is true and accurate.', termsAndConditions: 'terms and conditions.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1928ebf1be2c..7fe17fc771ce 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2355,6 +2355,7 @@ const translations = { agreementsStep: { agreements: 'Acuerdos', pleaseConfirm: 'Por favor confirme los acuerdos a continuación', + regulationRequiresUs: 'La normativa requiere que verifiquemos la identidad de cualquier individuo que posea más del 25% del negocio.', iAmAuthorized: 'Estoy autorizado para usar la cuenta bancaria para gastos del negocio.', iCertify: 'Certifico que la información proporcionada es verdadera y correcta.', termsAndConditions: 'términos y condiciones.', diff --git a/src/pages/ReimbursementAccount/NonUSD/Agreements/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/Agreements/substeps/Confirmation.tsx index f9d16a932e47..3a85089c55eb 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Agreements/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Agreements/substeps/Confirmation.tsx @@ -83,6 +83,7 @@ function Confirmation({onNext}: SubStepProps) { enabledWhenOffline={false} > {translate('agreementsStep.pleaseConfirm')} + {translate('agreementsStep.regulationRequiresUs')} {translate(shouldGatherBothEmails ? 'signerInfoStep.enterTwoEmails' : 'signerInfoStep.enterOneEmail')} + {!shouldGatherBothEmails && {translate('signerInfoStep.regulationRequiresOneMoreDirector')}} Date: Mon, 18 Nov 2024 06:07:01 -0800 Subject: [PATCH 328/421] docs: update `storybook-example.png` reference Signed-off-by: Emmanuel Ferdman --- contributingGuides/STORYBOOK.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributingGuides/STORYBOOK.md b/contributingGuides/STORYBOOK.md index bef9c6518deb..93815bab3da9 100644 --- a/contributingGuides/STORYBOOK.md +++ b/contributingGuides/STORYBOOK.md @@ -62,7 +62,7 @@ export { That will give us an interactive playground to test out various component attributes with the defaults we passed. -![Storybook example](web/storybook-example.png) +![Storybook example](/web/storybook-example.png) Note that we did not need to write any of the descriptions for these props. This is because they are automatically generated from a React component's `propTypes`. From 5e30c9b86895ac20ca0bf6d97fc7c6d5c8e606bc Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 18 Nov 2024 15:12:10 +0100 Subject: [PATCH 329/421] Reconnect from last udpate id --- src/components/InitialURLContextProvider.tsx | 24 ++++++++++++++++---- src/libs/actions/Session/index.ts | 4 ++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx index adf361a2573d..5290abf66f47 100644 --- a/src/components/InitialURLContextProvider.tsx +++ b/src/components/InitialURLContextProvider.tsx @@ -1,8 +1,11 @@ import React, {createContext, useEffect, useMemo, useState} from 'react'; import type {ReactNode} from 'react'; import {Linking} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import {signInAfterTransitionFromOldDot} from '@libs/actions/Session'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import {useSplashScreenStateContext} from '@src/SplashScreenStateContext'; @@ -17,7 +20,12 @@ const InitialURLContext = createContext({ setInitialURL: () => {}, }); -type InitialURLContextProviderProps = { +type InitialURLContextProviderPropsFromOnyx = { + /** In order to reconnect ND in hybrid app from right place */ + initialLastUpdateIDAppliedToClient: OnyxEntry; +}; + +type InitialURLContextProviderProps = InitialURLContextProviderPropsFromOnyx & { /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ url?: Route; @@ -25,13 +33,13 @@ type InitialURLContextProviderProps = { children: ReactNode; }; -function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) { +function InitialURLContextProvider({children, url, initialLastUpdateIDAppliedToClient}: InitialURLContextProviderProps) { const [initialURL, setInitialURL] = useState(); const {setSplashScreenState} = useSplashScreenStateContext(); useEffect(() => { if (url) { - signInAfterTransitionFromOldDot(url).then((route) => { + signInAfterTransitionFromOldDot(url, initialLastUpdateIDAppliedToClient).then((route) => { setInitialURL(route); setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN); }); @@ -40,7 +48,7 @@ function InitialURLContextProvider({children, url}: InitialURLContextProviderPro Linking.getInitialURL().then((initURL) => { setInitialURL(initURL as Route); }); - }, [setSplashScreenState, url]); + }, [initialLastUpdateIDAppliedToClient, setSplashScreenState, url]); const initialUrlContext = useMemo( () => ({ @@ -55,5 +63,11 @@ function InitialURLContextProvider({children, url}: InitialURLContextProviderPro InitialURLContextProvider.displayName = 'InitialURLContextProvider'; -export default InitialURLContextProvider; +// this `withOnyx` is used intentionally because we want to delay rendering until the last update's id is available for the app +export default withOnyx({ + initialLastUpdateIDAppliedToClient: { + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + }, +})(InitialURLContextProvider); + export {InitialURLContext}; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index a867bfcd7e36..2efba1a63393 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -481,7 +481,7 @@ function signUpUser() { API.write(WRITE_COMMANDS.SIGN_UP_USER, params, {optimisticData, successData, failureData}); } -function signInAfterTransitionFromOldDot(transitionURL: string) { +function signInAfterTransitionFromOldDot(transitionURL: string, lastUpdateId?: number) { const [route, queryParams] = transitionURL.split('?'); const { @@ -533,7 +533,7 @@ function signInAfterTransitionFromOldDot(transitionURL: string) { // We clear Onyx when this flag is set to true so we have to download all data App.openApp(); } else { - App.reconnectApp(); + App.reconnectApp(lastUpdateId); } }) .catch((error) => { From 1c77db756d6cad3dcadde8eddd318a177d43f0d7 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 18 Nov 2024 15:28:28 +0100 Subject: [PATCH 330/421] update force upgrade modal --- src/libs/actions/AppUpdate/updateApp/index.android.ts | 6 +++++- src/libs/actions/AppUpdate/updateApp/index.desktop.ts | 3 ++- src/libs/actions/AppUpdate/updateApp/index.ios.ts | 6 +++++- src/libs/actions/AppUpdate/updateApp/index.ts | 3 ++- src/pages/ErrorPage/UpdateRequiredView.tsx | 5 ++++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/AppUpdate/updateApp/index.android.ts b/src/libs/actions/AppUpdate/updateApp/index.android.ts index 3f2cde77f466..aac98a1928aa 100644 --- a/src/libs/actions/AppUpdate/updateApp/index.android.ts +++ b/src/libs/actions/AppUpdate/updateApp/index.android.ts @@ -1,6 +1,10 @@ import {Linking, NativeModules} from 'react-native'; import CONST from '@src/CONST'; -export default function updateApp() { +export default function updateApp(isProduction: boolean) { + if (isProduction) { + Linking.openURL(CONST.APP_DOWNLOAD_LINKS.OLD_DOT_ANDROID); + return; + } Linking.openURL(NativeModules.HybridAppModule ? CONST.APP_DOWNLOAD_LINKS.OLD_DOT_ANDROID : CONST.APP_DOWNLOAD_LINKS.ANDROID); } diff --git a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts index 5c1ecbe05742..cbd961ff653b 100644 --- a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts +++ b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts @@ -1,5 +1,6 @@ import ELECTRON_EVENTS from '@desktop/ELECTRON_EVENTS'; -export default function updateApp() { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function updateApp(isProduction: boolean) { window.electron.send(ELECTRON_EVENTS.SILENT_UPDATE); } diff --git a/src/libs/actions/AppUpdate/updateApp/index.ios.ts b/src/libs/actions/AppUpdate/updateApp/index.ios.ts index 930a57881128..608c7ab028ca 100644 --- a/src/libs/actions/AppUpdate/updateApp/index.ios.ts +++ b/src/libs/actions/AppUpdate/updateApp/index.ios.ts @@ -1,6 +1,10 @@ import {Linking, NativeModules} from 'react-native'; import CONST from '@src/CONST'; -export default function updateApp() { +export default function updateApp(isProduction: boolean) { + if (isProduction) { + Linking.openURL(CONST.APP_DOWNLOAD_LINKS.OLD_DOT_IOS); + return; + } Linking.openURL(NativeModules.HybridAppModule ? CONST.APP_DOWNLOAD_LINKS.OLD_DOT_IOS : CONST.APP_DOWNLOAD_LINKS.IOS); } diff --git a/src/libs/actions/AppUpdate/updateApp/index.ts b/src/libs/actions/AppUpdate/updateApp/index.ts index 8c2b191029a2..3b6d9e666bfa 100644 --- a/src/libs/actions/AppUpdate/updateApp/index.ts +++ b/src/libs/actions/AppUpdate/updateApp/index.ts @@ -1,6 +1,7 @@ /** * On web or mWeb we can simply refresh the page and the user should have the new version of the app downloaded. */ -export default function updateApp() { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function updateApp(isProduction: boolean) { window.location.reload(); } diff --git a/src/pages/ErrorPage/UpdateRequiredView.tsx b/src/pages/ErrorPage/UpdateRequiredView.tsx index 494ff4899887..9f3c4b224759 100644 --- a/src/pages/ErrorPage/UpdateRequiredView.tsx +++ b/src/pages/ErrorPage/UpdateRequiredView.tsx @@ -6,6 +6,7 @@ import HeaderGap from '@components/HeaderGap'; import Lottie from '@components/Lottie'; import LottieAnimations from '@components/LottieAnimations'; import Text from '@components/Text'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; @@ -19,6 +20,8 @@ function UpdateRequiredView() { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isProduction} = useEnvironment(); + return ( @@ -47,7 +50,7 @@ function UpdateRequiredView() {