Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c239c53
Add a skeleton test
tgolen Apr 24, 2025
4e54259
Add first assertion
tgolen Apr 24, 2025
c68f9de
Refactor test to be DRY and add more assertions
tgolen Apr 24, 2025
7fb5062
More test organizing
tgolen Apr 24, 2025
3343b78
Rename variables and use task reports
tgolen Apr 24, 2025
e5034c3
Finish writing tests to cover current logic
tgolen Apr 24, 2025
9faaf6e
fix typo
tgolen Apr 24, 2025
df02805
Refactor parameters for parent report and is archived
tgolen Apr 24, 2025
16fc9c6
Add new hook
tgolen Apr 24, 2025
54bd38d
Implement hooks in the tests
tgolen Apr 24, 2025
18ed22e
Remove unnecessary parameters
tgolen Apr 24, 2025
984742d
Add comment
tgolen Apr 24, 2025
f85e169
Make second param optional
tgolen Apr 24, 2025
d46050f
Implement new method signature
tgolen Apr 24, 2025
7d1a00f
Implement new method signature
tgolen Apr 24, 2025
9925873
Implement new method sig
tgolen Apr 24, 2025
e300db9
Revert changes
tgolen Apr 24, 2025
d097f82
Revert revert
tgolen Apr 24, 2025
81d5193
Remove unused variable
tgolen Apr 24, 2025
dbb51ec
Remove default value
tgolen Apr 24, 2025
8377e3d
Finish implementing
tgolen Apr 24, 2025
4bda56c
Remove unused variables
tgolen Apr 24, 2025
19bde87
Clean up tests
tgolen Apr 24, 2025
f147cd0
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen Apr 24, 2025
e6286c1
Exit early if no parent report
tgolen Apr 24, 2025
5c829c8
properly filter task reports
JS00001 Apr 25, 2025
6f6665a
Fix canModifyTask
tgolen Apr 25, 2025
aabb0ca
Refactor report details
tgolen Apr 25, 2025
bcef9dd
Refactor task view
tgolen Apr 25, 2025
d58ddcd
Refactor FAB
tgolen Apr 25, 2025
8b5f234
Refactor empty search view
tgolen Apr 25, 2025
4676fd6
Rename variables
tgolen Apr 25, 2025
51a3f14
Refactor assignee selector
tgolen Apr 25, 2025
4876708
Refactor task description page
tgolen Apr 25, 2025
f140784
Refactor task title page
tgolen Apr 25, 2025
4c386c4
Refactor task header action button
tgolen Apr 25, 2025
521e11f
Refactor task preview
tgolen Apr 25, 2025
4f7ae91
Refactor item row
tgolen Apr 25, 2025
1634e49
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen Apr 28, 2025
735db79
ESLint
tgolen Apr 28, 2025
f972b05
Add comments and address review requests
tgolen Apr 28, 2025
52e276a
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen Apr 28, 2025
4270a6b
Style
tgolen Apr 28, 2025
01827bb
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 8, 2025
61bed2e
debug test
tgolen May 8, 2025
ecfb4b4
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 8, 2025
cc48c61
Change back to each
tgolen May 8, 2025
977f6d1
Remove debug
tgolen May 8, 2025
39be9ed
Lint
tgolen May 8, 2025
0917b6d
TS
tgolen May 8, 2025
e1448ce
Jest
tgolen May 8, 2025
4f169c6
Remove extra line
tgolen May 8, 2025
af3572e
Lint
tgolen May 8, 2025
b24b424
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 26, 2025
d88e2d5
Lint
tgolen May 26, 2025
cd040b4
Remove an extra mock definition
tgolen May 26, 2025
02b87fd
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 27, 2025
c318ae1
Remove unnecessary mock
tgolen May 27, 2025
29cac38
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 28, 2025
515ee08
Merge branch 'main' into tgolen-remove-rnvp-4
tgolen May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ jest.mock('@libs/prepareRequestPayload/index.native.ts', () => ({
}),
}));

// This keeps the error "@rnmapbox/maps native code not available." from causing the tests to fail
jest.mock('@components/ConfirmedRoute.tsx');

jest.mock('@src/hooks/useWorkletStateMachine/executeOnUIRuntimeSync', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useParentReport from '@hooks/useParentReport';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useThemeStyles from '@hooks/useThemeStyles';
import {getInternalExpensifyPath, getInternalNewExpensifyPath, openExternalLink, openLink} from '@libs/actions/Link';
import {isAnonymousUser} from '@libs/actions/Session';
Expand All @@ -36,8 +38,10 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
selector: hasSeenTourSelector,
canBeMissing: true,
});
const canModifyViewTourTask = canModifyTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
const canActionViewTourTask = canActionTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
const parentReport = useParentReport(report?.reportID);
const isParentReportArchived = useReportIsArchived(parentReport?.reportID);
Comment thread
JS00001 marked this conversation as resolved.
const canModifyViewTourTask = canModifyTask(viewTourTaskReport, currentUserPersonalDetails.accountID, isParentReportArchived);
const canActionViewTourTask = canActionTask(viewTourTaskReport, currentUserPersonalDetails.accountID, parentReport, isParentReportArchived);

const styles = useThemeStyles();
const htmlAttribs = tnode.attributes;
Expand Down
8 changes: 6 additions & 2 deletions src/components/ReportActionItem/TaskPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useParentReport from '@hooks/useParentReport';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -93,7 +95,9 @@ function TaskPreview({
? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskAssigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const taskOwnerAccountID = taskReport?.ownerAccountID ?? action?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
const parentReport = useParentReport(taskReport?.reportID);
const isParentReportArchived = useReportIsArchived(parentReport?.reportID);
const isTaskActionable = canActionTask(taskReport, currentUserPersonalDetails.accountID, parentReport, isParentReportArchived);
const hasAssignee = taskAssigneeAccountID > 0;
const personalDetails = usePersonalDetails();
const avatar = personalDetails?.[taskAssigneeAccountID]?.avatar ?? Expensicons.FallbackAvatar;
Expand Down Expand Up @@ -138,7 +142,7 @@ function TaskPreview({
<Checkbox
style={[styles.mr2]}
isChecked={isTaskCompleted}
disabled={!canActionTask(taskReport, currentUserPersonalDetails.accountID, taskOwnerAccountID, taskAssigneeAccountID)}
disabled={!isTaskActionable}
onPress={callFunctionIfActionIsAllowed(() => {
if (isTaskCompleted) {
reopenTask(taskReport, taskReportID);
Expand Down
16 changes: 10 additions & 6 deletions src/components/ReportActionItem/TaskView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {ShowContextMenuContext} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useParentReport from '@hooks/useParentReport';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import getButtonState from '@libs/getButtonState';
Expand All @@ -26,7 +28,7 @@ import {getDisplayNameForParticipant, getDisplayNamesWithTooltips, isCompletedTa
import StringUtils from '@libs/StringUtils';
import {isActiveTaskEditRoute} from '@libs/TaskUtils';
import {callFunctionIfActionIsAllowed} from '@userActions/Session';
import {canActionTask as canActionTaskUtil, canModifyTask as canModifyTaskUtil, clearTaskErrors, completeTask, reopenTask, setTaskReport} from '@userActions/Task';
import {canActionTask, canModifyTask, clearTaskErrors, completeTask, reopenTask, setTaskReport} from '@userActions/Task';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report, ReportAction} from '@src/types/onyx';
Expand Down Expand Up @@ -57,11 +59,13 @@ function TaskView({report, action}: TaskViewProps) {

const isOpen = isOpenTaskReport(report);
const isCompleted = isCompletedTaskReport(report);
const canModifyTask = canModifyTaskUtil(report, currentUserPersonalDetails.accountID);
const canActionTask = canActionTaskUtil(report, currentUserPersonalDetails.accountID);
const parentReport = useParentReport(report?.reportID);
const isParentReportArchived = useReportIsArchived(parentReport?.reportID);
const isTaskModifiable = canModifyTask(report, currentUserPersonalDetails.accountID, isParentReportArchived);
const isTaskActionable = canActionTask(report, currentUserPersonalDetails.accountID, parentReport, isParentReportArchived);

const disableState = !canModifyTask;
const isDisableInteractive = !canModifyTask || !isOpen;
const disableState = !isTaskModifiable;
const isDisableInteractive = disableState || !isOpen;
const {translate} = useLocalize();
const accountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID;
const contextValue = useMemo(
Expand Down Expand Up @@ -134,7 +138,7 @@ function TaskView({report, action}: TaskViewProps) {
containerBorderRadius={8}
caretSize={16}
accessibilityLabel={taskTitle || translate('task.task')}
disabled={!canActionTask}
disabled={!isTaskActionable}
/>
<View style={[styles.flexRow, styles.flex1]}>
<RenderHTML html={taskTitle} />
Expand Down
10 changes: 6 additions & 4 deletions src/components/SelectionList/Search/TaskListItemRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {useSession} from '@components/OnyxProvider';
import type {TaskListItemType} from '@components/SelectionList/types';
import TextWithTooltip from '@components/TextWithTooltip';
import useLocalize from '@hooks/useLocalize';
import useParentReport from '@hooks/useParentReport';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
Expand Down Expand Up @@ -68,9 +70,9 @@ function ActionCell({taskItem, isLargeScreenWidth}: TaskCellProps) {
const StyleUtils = useStyleUtils();
const session = useSession();
const {translate} = useLocalize();

const taskAssigneeID = taskItem.assignee.accountID;
const taskCreatorID = taskItem.createdBy.accountID;
const parentReport = useParentReport(taskItem?.report?.reportID);
const isParentReportArchived = useReportIsArchived(parentReport?.reportID);
const isTaskActionable = canActionTask(taskItem.report, session?.accountID, parentReport, isParentReportArchived);
const isTaskCompleted = taskItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED && taskItem.stateNum === CONST.REPORT.STATE_NUM.APPROVED;

if (isTaskCompleted) {
Expand Down Expand Up @@ -102,7 +104,7 @@ function ActionCell({taskItem, isLargeScreenWidth}: TaskCellProps) {
success
text={translate('task.action')}
style={[styles.w100]}
isDisabled={!canActionTask(taskItem.report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, taskCreatorID, taskAssigneeID)}
isDisabled={!isTaskActionable}
onPress={callFunctionIfActionIsAllowed(() => {
completeTask(taskItem, taskItem.reportID);
})}
Expand Down
8 changes: 6 additions & 2 deletions src/components/TaskHeaderActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useParentReport from '@hooks/useParentReport';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useThemeStyles from '@hooks/useThemeStyles';
import {canWriteInReport, isCompletedTaskReport} from '@libs/ReportUtils';
import {isActiveTaskEditRoute} from '@libs/TaskUtils';
import {callFunctionIfActionIsAllowed} from '@userActions/Session';
import {canActionTask, completeTask, reopenTask} from '@userActions/Task';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';
import Button from './Button';
import {useSession} from './OnyxProvider';
Expand All @@ -20,6 +21,9 @@ function TaskHeaderActionButton({report}: TaskHeaderActionButtonProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const session = useSession();
const parentReport = useParentReport(report.reportID);
const isParentReportArchived = useReportIsArchived(parentReport?.reportID);
const isTaskActionable = canActionTask(report, session?.accountID, parentReport, isParentReportArchived);

if (!canWriteInReport(report)) {
return null;
Expand All @@ -29,7 +33,7 @@ function TaskHeaderActionButton({report}: TaskHeaderActionButtonProps) {
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Button
success
isDisabled={!canActionTask(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID)}
isDisabled={!isTaskActionable}
text={translate(isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')}
onPress={callFunctionIfActionIsAllowed(() => {
// If we're already navigating to these task editing pages, early return not to mark as completed, otherwise we would have not found page.
Expand Down
12 changes: 12 additions & 0 deletions src/hooks/useParentReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {OnyxEntry} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
import useOnyx from './useOnyx';

function useParentReport(reportID?: string): OnyxEntry<Report> {
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true});
Comment thread
JS00001 marked this conversation as resolved.
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`, {canBeMissing: true});
Comment thread
JS00001 marked this conversation as resolved.
return parentReport;
}

export default useParentReport;
53 changes: 41 additions & 12 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ Onyx.connect({
callback: (val) => (introSelected = val),
});

let allReportNameValuePair: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
if (!value) {
return;
}
allReportNameValuePair = value;
},
});

/**
* Clears out the task info from the store
*/
Expand Down Expand Up @@ -998,6 +1010,7 @@ function getParentReportAction(report: OnyxEntry<OnyxTypes.Report>): OnyxEntry<R
if (!report?.parentReportID || !report.parentReportActionID) {
return undefined;
}

return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
}

Expand Down Expand Up @@ -1221,7 +1234,11 @@ function getTaskAssigneeAccountID(taskReport: OnyxEntry<OnyxTypes.Report>): numb
/**
* Check if you're allowed to modify the task - only the author can modify the task
*/
function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number, isParentReportArchived = false): boolean {
function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID?: number, isParentReportArchived = false): boolean {
if (!sessionAccountID) {
return false;
}

if (ReportUtils.isCanceledTaskReport(taskReport)) {
return false;
}
Expand All @@ -1236,23 +1253,32 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID
/**
* Check if you can change the status of the task (mark complete or incomplete). Only the task owner and task assignee can do this.
*/
function canActionTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number, taskOwnerAccountID?: number, taskAssigneeAccountID?: number): boolean {
if (ReportUtils.isCanceledTaskReport(taskReport)) {
function canActionTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID?: number, parentReport?: OnyxEntry<OnyxTypes.Report>, isParentReportArchived = false): boolean {
// Return early if there was no sessionAccountID (this can happen because when connecting to the session key in onyx, the session will be undefined initially)
if (!sessionAccountID) {
return false;
}

const parentReport = getParentReport(taskReport);
// When there is no parent report, exit early (this can also happen due to onyx key initialization)
if (!parentReport) {
return false;
Comment on lines +1263 to +1264

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from #65041 checklist:
After the user leaves the chat, parentReport becomes unavailable. However, the assigneeAccountID may still be present. As a result, the previous condition sessionAccountID === assigneeAccountID will be skipped

}

// Cancelled task reports cannot be actioned since they are cancelled and users shouldn't be able to do anything with them
if (ReportUtils.isCanceledTaskReport(taskReport)) {
return false;
}

// This will get removed as part of https://github.com/Expensify/App/issues/59961
// eslint-disable-next-line deprecation/deprecation
const reportNameValuePairs = ReportUtils.getReportNameValuePairs(parentReport?.reportID);
if (ReportUtils.isArchivedNonExpenseReport(parentReport, reportNameValuePairs)) {
// If the parent report is a non expense report and it's archived, then the user cannot take actions (similar to cancelled task reports)
const isParentAnExpenseReport = ReportUtils.isExpenseReport(parentReport);
const isParentAnExpenseRequest = ReportUtils.isExpenseRequest(parentReport);
const isParentANonExpenseReport = !(isParentAnExpenseReport || isParentAnExpenseRequest);
if (isParentANonExpenseReport && isParentReportArchived) {
return false;
}

const ownerAccountID = taskReport?.ownerAccountID ?? taskOwnerAccountID;
const assigneeAccountID = getTaskAssigneeAccountID(taskReport) ?? taskAssigneeAccountID;
return sessionAccountID === ownerAccountID || sessionAccountID === assigneeAccountID;
// The task can only be actioned by the task report owner or the task assignee
return sessionAccountID === taskReport?.ownerAccountID || sessionAccountID === getTaskAssigneeAccountID(taskReport);
}

function clearTaskErrors(reportID: string | undefined) {
Expand All @@ -1279,7 +1305,10 @@ function clearTaskErrors(reportID: string | undefined) {
function getFinishOnboardingTaskOnyxData(taskName: IntroSelectedTask): OnyxData {
const taskReportID = introSelected?.[taskName];
const taskReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`];
if (taskReportID && canActionTask(taskReport, currentUserAccountID)) {
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReport?.parentReportID}`];
const parentReportNameValuePairs = allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${parentReport?.reportID}`];
const isParentReportArchived = ReportUtils.isArchivedReport(parentReportNameValuePairs);
if (taskReportID && canActionTask(taskReport, currentUserAccountID, parentReport, isParentReportArchived)) {
if (taskReport) {
if (taskReport.stateNum !== CONST.REPORT.STATE_NUM.APPROVED || taskReport.statusNum !== CONST.REPORT.STATUS_NUM.APPROVED) {
return completeTask(taskReport);
Expand Down
Loading