Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a986e71
bring auditor role to NewDot
hungvu193 Aug 16, 2024
fec77ba
fix linting
hungvu193 Aug 17, 2024
0f6423c
refactor bulkaction logic
hungvu193 Aug 17, 2024
5b59e2c
fix lint
hungvu193 Aug 17, 2024
c34ae3c
update auditor role for expense
hungvu193 Aug 20, 2024
f08c970
remove local changes
hungvu193 Aug 20, 2024
775b47d
merge main
hungvu193 Aug 20, 2024
79c7b28
Merge remote-tracking branch 'origin/main' into feat/bring-auditor-ro…
hungvu193 Aug 21, 2024
97df3fd
prevent auditor from editing room description
hungvu193 Aug 22, 2024
740fde0
Merge remote-tracking branch 'origin/main' into feat/bring-auditor-ro…
hungvu193 Aug 22, 2024
59abfac
update icon and rename isOnlyAllowedToComment
hungvu193 Aug 23, 2024
a9ca0d6
merge main
hungvu193 Aug 23, 2024
429cac3
fix linting
hungvu193 Aug 23, 2024
e5fb2b7
add Auditor badge
hungvu193 Aug 23, 2024
e717b1f
Merge branch 'feat/bring-auditor-role-to-nd' of https://github.com/hu…
hungvu193 Aug 24, 2024
32c5d74
fix lint
hungvu193 Aug 24, 2024
dfd6e5f
merge main
hungvu193 Sep 4, 2024
608d3c1
Merge branch 'main' into feat/bring-auditor-role-to-nd
hungvu193 Sep 4, 2024
3a64f82
address review comment
hungvu193 Sep 6, 2024
e31a362
Merge branch 'feat/bring-auditor-role-to-nd' of github.com:hungvu193/…
hungvu193 Sep 6, 2024
b286926
address lint
hungvu193 Sep 6, 2024
ba0cff2
Merge remote-tracking branch 'origin/main' into feat/bring-auditor-ro…
hungvu193 Sep 6, 2024
5109afa
address comment
hungvu193 Sep 7, 2024
419780a
Merge remote-tracking branch 'origin/main' into feat/bring-auditor-ro…
hungvu193 Sep 9, 2024
dcfe404
Merge branch 'feat/bring-auditor-role-to-nd' of github.com:hungvu193/…
hungvu193 Sep 9, 2024
1d6aecc
address comment
hungvu193 Sep 10, 2024
e6ff8f1
merge main
hungvu193 Sep 12, 2024
e6bd7aa
update isAudiot func
hungvu193 Sep 13, 2024
bd55006
Merge remote-tracking branch 'origin/main' into feat/bring-auditor-ro…
hungvu193 Sep 13, 2024
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
12 changes: 12 additions & 0 deletions assets/images/user-eye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,7 @@ const CONST = {
REMOVE: 'remove',
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
MAKE_AUDITOR: 'makeAuditor',
},
BULK_ACTION_TYPES: {
DELETE: 'delete',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ import Unlock from '@assets/images/unlock.svg';
import UploadAlt from '@assets/images/upload-alt.svg';
import Upload from '@assets/images/upload.svg';
import UserCheck from '@assets/images/user-check.svg';
import UserEye from '@assets/images/user-eye.svg';
import UserPlus from '@assets/images/user-plus.svg';
import User from '@assets/images/user.svg';
import Users from '@assets/images/users.svg';
Expand Down Expand Up @@ -392,6 +393,7 @@ export {
Filters,
CalendarSolid,
Filter,
UserEye,
CaretUpDown,
UserPlus,
Feed,
Expand Down
17 changes: 17 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ export default {
default: 'Default',
update: 'Update',
member: 'Member',
auditor: 'Auditor',
role: 'Role',
currency: 'Currency',
rate: 'Rate',
Expand Down Expand Up @@ -2225,6 +2226,21 @@ export default {
reuseExistingConnection: 'Reuse existing connection',
existingConnections: 'Existing connections',
lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Last synced ${formattedDate}`,
memberAlternateText: 'Members can submit and approve reports.',
adminAlternateText: 'Admins have full edit access to all reports and workspace settings.',
auditorAlternateText: 'Auditors can view and comment on reports.',
roleName: (role?: string): string => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
return 'Admin';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.USER:
return 'Member';
default:
return 'Member';
}
},
},
qbo: {
importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.',
Expand Down Expand Up @@ -3214,6 +3230,7 @@ export default {
transferOwner: 'Transfer owner',
makeMember: 'Make member',
makeAdmin: 'Make admin',
makeAuditor: 'Make auditor',
selectAll: 'Select all',
error: {
genericAdd: 'There was a problem adding this workspace member.',
Expand Down
17 changes: 17 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ export default {
default: 'Predeterminado',
update: 'Actualizar',
member: 'Miembro',
auditor: 'Auditor',
role: 'Role',
currency: 'Divisa',
rate: 'Tarifa',
Expand Down Expand Up @@ -2256,6 +2257,21 @@ export default {
existingConnections: 'Conexiones existentes',
lastSyncDate: (connectionName: string, formattedDate: string) => `${connectionName} - Última sincronización ${formattedDate}`,
topLevel: 'Nivel superior',
memberAlternateText: 'Los miembros pueden presentar y aprobar informes.',
Comment thread
hungvu193 marked this conversation as resolved.
adminAlternateText: 'Los administradores tienen acceso total para editar todos los informes y la configuración del área de trabajo.',
auditorAlternateText: 'Los auditores pueden ver y comentar los informes.',
roleName: (role?: string): string => {
switch (role) {
case CONST.POLICY.ROLE.ADMIN:
return 'Administrador';
case CONST.POLICY.ROLE.AUDITOR:
return 'Auditor';
case CONST.POLICY.ROLE.USER:
return 'Miembro';
default:
return 'Miembro';
}
},
},
qbo: {
importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.',
Expand Down Expand Up @@ -3264,6 +3280,7 @@ export default {
transferOwner: 'Transferir la propiedad',
makeMember: 'Hacer miembro',
makeAdmin: 'Hacer administrador',
makeAuditor: 'Hacer auditor',
selectAll: 'Seleccionar todo',
error: {
genericAdd: 'Ha ocurrido un problema al añadir el miembro al espacio de trabajo.',
Expand Down
7 changes: 7 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ const isPolicyAdmin = (policy: OnyxInputOrEntry<Policy>, currentUserLogin?: stri
*/
const isPolicyUser = (policy: OnyxInputOrEntry<Policy>, currentUserLogin?: string): boolean => getPolicyRole(policy, currentUserLogin) === CONST.POLICY.ROLE.USER;

/**
* Checks if the current user is an auditor of the policy
*/
const isPolicyAuditor = (policy: OnyxInputOrEntry<Policy>, currentUserLogin?: string): boolean =>
(policy?.role ?? (currentUserLogin && policy?.employeeList?.[currentUserLogin]?.role)) === CONST.POLICY.ROLE.AUDITOR;

/**
* Checks if the policy is a free group policy.
*/
Expand Down Expand Up @@ -1035,6 +1041,7 @@ export {
isPendingDeletePolicy,
isPolicyAdmin,
isPolicyUser,
isPolicyAuditor,
isPolicyEmployee,
isPolicyFeatureEnabled,
isPolicyOwner,
Expand Down
28 changes: 25 additions & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1415,12 +1415,28 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean {
return ReportActionsUtils.isActionableJoinRequestPending(report.reportID);
}

/**
* Checks if the user has auditor permission in the provided report
*/
function isAuditor(report: OnyxEntry<Report>): boolean {

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.

Okay so I think this is slightly wrong - I think we should check for isPolicyAuditor first and THEN check report permissions afterwards. Otherwise the following test steps seem to occur:

  1. Create a workspace
  2. Add two members, then make one an admin and one an auditor
  3. Assign a task to each of them in the #admins chat
  4. Log in as the auditor
  5. The auditor will be able to edit both tasks.

I looked at Onyx and the auditor's report permissions for the task assigned to the admin is read, write, share. I'm honestly not sure why that is, but it seems like if we checked isPolicyAuditor first then we wouldn't get to that point.

What do you think? Would this cause any issues I'm not aware of?

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.

Follow up - let me know how you were testing it before if it was seeming to work then? Maybe I did something wrong - or maybe there's a flow (maybe the change role isn't working or something?) that we need to adjust

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks let me take a look

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's pretty weird that on the admin assigned task, when I viewed it as an auditor the permissions are read, write, share which makes isAuditor return false. Meanwhile it should be empty (as I checked/ tested before 😂).

Switch the PolicyUtils.isPolicyAuditor(policy); to the top will make it works because we will use the role to check it. Nevermind, let me update the code.

Thanks for testing it! 💯

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated!

if (report?.policyID) {
const policy = getPolicy(report.policyID);
return PolicyUtils.isPolicyAuditor(policy);
}

if (Array.isArray(report?.permissions) && report?.permissions.length > 0) {
return report?.permissions?.includes(CONST.REPORT.PERMISSIONS.AUDITOR);
}

return false;
}

/**
* Checks if the user can write in the provided report
*/
function canWriteInReport(report: OnyxEntry<Report>): boolean {
if (Array.isArray(report?.permissions) && report?.permissions.length > 0) {
return report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) || (report?.permissions?.includes(CONST.REPORT.PERMISSIONS.AUDITOR) && isExpenseReport(report));
if (Array.isArray(report?.permissions) && report?.permissions.length > 0 && !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.AUDITOR)) {
return report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE);
}

return true;
Expand All @@ -1430,6 +1446,10 @@ function canWriteInReport(report: OnyxEntry<Report>): boolean {
* Checks if the current user is allowed to comment on the given report.
*/
function isAllowedToComment(report: OnyxEntry<Report>): boolean {
if (isAuditor(report)) {
return true;

@QichenZhu QichenZhu Jul 28, 2025

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.

Auditors could see the composer in #announce rooms but shouldn't because only admins are allowed to comment (issue #65511). Fixed in #66211.

}

if (!canWriteInReport(report)) {
return false;
}
Expand Down Expand Up @@ -7075,7 +7095,8 @@ function canEditReportDescription(report: OnyxEntry<Report>, policy: OnyxEntry<P
isChatRoom(report) &&
!isChatThread(report) &&
!isEmpty(policy) &&
hasParticipantInArray(report, [currentUserAccountID ?? 0])
hasParticipantInArray(report, [currentUserAccountID ?? 0]) &&
!isAuditor(report)
);
}

Expand Down Expand Up @@ -8108,6 +8129,7 @@ export {
getArchiveReason,
getApprovalChain,
isIndividualInvoiceRoom,
isAuditor,
hasMissingInvoiceBankAccount,
};

Expand Down
5 changes: 3 additions & 2 deletions src/libs/actions/Policy/Member.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import type {
AddMembersToWorkspaceParams,
Expand Down Expand Up @@ -41,7 +42,7 @@ type NewCustomUnit = {
type WorkspaceMembersRoleData = {
accountID: number;
email: string;
role: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
role: ValueOf<typeof CONST.POLICY.ROLE>;
};

const allPolicies: OnyxCollection<Policy> = {};
Expand Down Expand Up @@ -407,7 +408,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData});
}

function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newRole: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER) {
function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newRole: ValueOf<typeof CONST.POLICY.ROLE>) {
const previousEmployeeList = {...allPolicies?.[policyID]?.employeeList};
const memberRoles: WorkspaceMembersRoleData[] = accountIDs.reduce((result: WorkspaceMembersRoleData[], accountID: number) => {
if (!allPersonalDetails?.[accountID]?.login) {
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ function getTaskOwnerAccountID(taskReport: OnyxEntry<OnyxTypes.Report>): number
}

/**
* Check if you're allowed to modify the task - anyone that has write access to the report can modify the task
* Check if you're allowed to modify the task - anyone that has write access to the report can modify the task, except auditor
*/
function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID: number): boolean {
if (ReportUtils.isCanceledTaskReport(taskReport)) {
Expand All @@ -1143,7 +1143,7 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID
return true;
}

if (!ReportUtils.canWriteInReport(taskReport)) {
if (!ReportUtils.canWriteInReport(taskReport) || ReportUtils.isAuditor(taskReport)) {
Comment thread
hungvu193 marked this conversation as resolved.
return false;
}

Expand Down
54 changes: 39 additions & 15 deletions src/pages/workspace/WorkspaceMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {TextInput} from 'react-native';
import {InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Badge from '@components/Badge';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
Expand Down Expand Up @@ -336,9 +337,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson

const isOwner = policy?.owner === details.login;
const isAdmin = policyEmployee.role === CONST.POLICY.ROLE.ADMIN;
const isAuditor = policyEmployee.role === CONST.POLICY.ROLE.AUDITOR;
let roleBadge = null;
if (isOwner || isAdmin) {
roleBadge = <Badge text={isOwner ? translate('common.owner') : translate('common.admin')} />;
} else if (isAuditor) {
roleBadge = <Badge text={translate('common.auditor')} />;
}

result.push({
Expand Down Expand Up @@ -450,7 +454,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
return <View style={[styles.peopleRow, styles.userSelectNone, styles.ph9, styles.pv3, styles.pb5]}>{header}</View>;
};

const changeUserRole = (role: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER) => {
const changeUserRole = (role: ValueOf<typeof CONST.POLICY.ROLE>) => {
if (!isEmptyObject(errors)) {
return;
}
Expand Down Expand Up @@ -482,23 +486,43 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson
const email = personalDetails?.[accountID]?.login ?? '';
return policy?.employeeList?.[email]?.role;
});
if (selectedEmployeesRoles.find((role) => role === CONST.POLICY.ROLE.ADMIN)) {
options.push({
text: translate('workspace.people.makeMember'),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER,
icon: Expensicons.User,
onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER),
});

const memberOption = {
text: translate('workspace.people.makeMember'),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER,
icon: Expensicons.User,
onSelected: () => changeUserRole(CONST.POLICY.ROLE.USER),
};
const adminOption = {
text: translate('workspace.people.makeAdmin'),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN,
icon: Expensicons.MakeAdmin,
onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN),
};

const auditorOption = {
text: translate('workspace.people.makeAuditor'),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_AUDITOR,
icon: Expensicons.UserEye,
onSelected: () => changeUserRole(CONST.POLICY.ROLE.AUDITOR),
};

const hasAtLeastOneNonAuditorRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.AUDITOR);
const hasAtLeastOneNonMemberRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.USER);
const hasAtLeastOneNonAdminRole = selectedEmployeesRoles.some((role) => role !== CONST.POLICY.ROLE.ADMIN);

if (hasAtLeastOneNonMemberRole) {
options.push(memberOption);
}

if (selectedEmployeesRoles.find((role) => role === CONST.POLICY.ROLE.USER)) {
options.push({
text: translate('workspace.people.makeAdmin'),
value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN,
icon: Expensicons.MakeAdmin,
onSelected: () => changeUserRole(CONST.POLICY.ROLE.ADMIN),
});
if (hasAtLeastOneNonAdminRole) {
options.push(adminOption);
}

if (hasAtLeastOneNonAuditorRole) {
options.push(auditorOption);
}

return options;
};

Expand Down
13 changes: 11 additions & 2 deletions src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,24 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
{
value: CONST.POLICY.ROLE.ADMIN,
text: translate('common.admin'),
alternateText: translate('workspace.common.adminAlternateText'),
isSelected: member?.role === CONST.POLICY.ROLE.ADMIN,
keyForList: CONST.POLICY.ROLE.ADMIN,
},
{
value: CONST.POLICY.ROLE.USER,
text: translate('common.member'),
isSelected: member?.role !== CONST.POLICY.ROLE.ADMIN,
alternateText: translate('workspace.common.memberAlternateText'),
isSelected: member?.role === CONST.POLICY.ROLE.USER,
keyForList: CONST.POLICY.ROLE.USER,
},
{
value: CONST.POLICY.ROLE.AUDITOR,
text: translate('common.auditor'),
alternateText: translate('workspace.common.auditorAlternateText'),
isSelected: member?.role === CONST.POLICY.ROLE.AUDITOR,
keyForList: CONST.POLICY.ROLE.AUDITOR,
},
],
[member?.role, translate],
);
Expand Down Expand Up @@ -247,7 +256,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM
<View style={styles.w100}>
<MenuItemWithTopDescription
disabled={isSelectedMemberOwner || isSelectedMemberCurrentUser}
title={member?.role === CONST.POLICY.ROLE.ADMIN ? translate('common.admin') : translate('common.member')}
title={translate(`workspace.common.roleName`, member?.role)}
description={translate('common.role')}
shouldShowRightIcon
onPress={openRoleSelectionModal}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -10,10 +11,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';

type ListItemType = {
value: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
value: ValueOf<typeof CONST.POLICY.ROLE>;
text: string;
alternateText: string;
isSelected: boolean;
keyForList: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER;
keyForList: ValueOf<typeof CONST.POLICY.ROLE>;
};

type WorkspaceMemberDetailsPageProps = {
Expand Down Expand Up @@ -56,6 +58,7 @@ function WorkspaceMemberDetailsRoleSelectionModal({isVisible, items, onRoleChang
sections={[{data: items}]}
ListItem={RadioListItem}
onSelectRow={onRoleChange}
isAlternateTextMultilineSupported
shouldSingleExecuteRowSelect
initiallyFocusedOptionKey={items.find((item) => item.isSelected)?.keyForList}
/>
Expand Down