Skip to content

Commit

Permalink
Merge pull request #35671 from rezkiy37/feature/35286-leave-workspace
Browse files Browse the repository at this point in the history
Add possibility to leave from workspaces and workspace expense chats
  • Loading branch information
MonilBhavsar authored Apr 15, 2024
2 parents b5fdc87 + 7321ad2 commit 2ce785c
Show file tree
Hide file tree
Showing 15 changed files with 216 additions and 13 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ const CONST = {
UPDATE_TAG_NAME: 'POLICYCHANGELOG_UPDATE_TAG_NAME',
UPDATE_TIME_ENABLED: 'POLICYCHANGELOG_UPDATE_TIME_ENABLED',
UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE',
LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY',
},
ROOMCHANGELOG: {
INVITE_TO_ROOM: 'INVITETOROOM',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,7 @@ export default {
users: 'users',
invited: 'invited',
removed: 'removed',
leftWorkspace: 'left the workspace',
to: 'to',
from: 'from',
},
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,7 @@ export default {
users: 'usuarios',
invited: 'invitó',
removed: 'eliminó',
leftWorkspace: 'salió del espacio de trabajo',
to: 'a',
from: 'de',
},
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/LeavePolicyParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type LeavePolicyParams = {
policyID: string;
email: string;
};

export default LeavePolicyParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,4 @@ export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicy
export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams';
export type {default as UpdatePolicyConnectionConfigParams} from './UpdatePolicyConnectionConfigParams';
export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams';
export type {default as LeavePolicyParams} from './LeavePolicyParams';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const WRITE_COMMANDS = {
UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue',
SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled',
DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates',
LEAVE_POLICY: 'LeavePolicy',
} as const;

type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -409,6 +410,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams;
[WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams;
[WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams;
};

const READ_COMMANDS = {
Expand Down
6 changes: 6 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ const isFreeGroupPolicy = (policy: OnyxEntry<Policy> | EmptyObject): boolean =>

const isPolicyMember = (policyID: string, policies: OnyxCollection<Policy>): boolean => Object.values(policies ?? {}).some((policy) => policy?.id === policyID);

/**
* Checks if the current user is an owner (creator) of the policy.
*/
const isPolicyOwner = (policy: OnyxEntry<Policy>, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID;

/**
* Create an object mapping member emails to their accountIDs. Filter for members without errors, and get the login email from the personalDetail object using the accountID.
*
Expand Down Expand Up @@ -341,6 +346,7 @@ export {
getCountOfEnabledTagsOfList,
isPendingDeletePolicy,
isPolicyMember,
isPolicyOwner,
isPaidGroupPolicy,
extractPolicyIDFromPath,
getPathWithoutPolicyID,
Expand Down
17 changes: 15 additions & 2 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,19 @@ function isMemberChangeAction(reportAction: OnyxEntry<ReportAction>) {
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.REMOVE_FROM_ROOM
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.REMOVE_FROM_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.LEAVE_POLICY
);
}

function isInviteMemberAction(reportAction: OnyxEntry<ReportAction>) {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM;
}

function isLeavePolicyAction(reportAction: OnyxEntry<ReportAction>) {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.LEAVE_POLICY;
}

function isReimbursementDeQueuedAction(reportAction: OnyxEntry<ReportAction>): reportAction is ReportActionBase & OriginalMessageReimbursementDequeued {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED;
}
Expand Down Expand Up @@ -861,9 +866,17 @@ function isNotifiableReportAction(reportAction: OnyxEntry<ReportAction>): boolea

function getMemberChangeMessageElements(reportAction: OnyxEntry<ReportAction>): readonly MemberChangeMessageElement[] {
const isInviteAction = isInviteMemberAction(reportAction);
const isLeaveAction = isLeavePolicyAction(reportAction);

// Currently, we only render messages when members are invited
const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed');
let verb = Localize.translateLocal('workspace.invite.removed');
if (isInviteAction) {
verb = Localize.translateLocal('workspace.invite.invited');
}

if (isLeaveAction) {
verb = Localize.translateLocal('workspace.invite.leftWorkspace');
}

const originalMessage = reportAction?.originalMessage as ChangeLog;
const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? [];
Expand Down
25 changes: 23 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4687,9 +4687,9 @@ function getChatByParticipantsAndPolicy(newParticipantList: number[], policyID:
if (!report?.participantAccountIDs) {
return false;
}
const sortedParticipanctsAccountIDs = report.participantAccountIDs?.sort();
const sortedParticipantsAccountIDs = report.participantAccountIDs?.sort();
// Only return the room if it has all the participants and is not a policy room
return report.policyID === policyID && lodashIsEqual(newParticipantList, sortedParticipanctsAccountIDs);
return report.policyID === policyID && newParticipantList.every((newParticipant) => sortedParticipantsAccountIDs.includes(newParticipant));
}) ?? null
);
}
Expand Down Expand Up @@ -5177,6 +5177,15 @@ function getWorkspaceChats(policyID: string, accountIDs: number[]): Array<OnyxEn
return Object.values(allReports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1));
}

/**
* Gets all reports that relate to the policy
*
* @param policyID - the workspace ID to get all associated reports
*/
function getAllWorkspaceReports(policyID: string): Array<OnyxEntry<Report>> {
return Object.values(allReports ?? {}).filter((report) => (report?.policyID ?? '') === policyID);
}

/**
* @param policy - the workspace the report is on, null if the user isn't a member of the workspace
*/
Expand Down Expand Up @@ -5761,6 +5770,11 @@ function canBeAutoReimbursed(report: OnyxEntry<Report>, policy: OnyxEntry<Policy
return isAutoReimbursable;
}

/** Check if the current user is an owner of the report */
function isReportOwner(report: OnyxEntry<Report>): boolean {
return report?.ownerAccountID === currentUserPersonalDetails?.accountID;
}

function isAllowedToApproveExpenseReport(report: OnyxEntry<Report>, approverAccountID?: number): boolean {
const policy = getPolicy(report?.policyID);
const {preventSelfApproval} = policy;
Expand Down Expand Up @@ -5820,6 +5834,10 @@ function hasActionsWithErrors(reportID: string): boolean {
return Object.values(reportActions).some((action) => !isEmptyObject(action.errors));
}

function canLeavePolicyExpenseChat(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>): boolean {
return isPolicyExpenseChat(report) && !(PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPolicyOwner(policy, currentUserAccountID ?? -1) || isReportOwner(report));
}

function getReportActionActorAccountID(reportAction: OnyxEntry<ReportAction>, iouReport: OnyxEntry<Report> | undefined): number | undefined {
switch (reportAction?.actionName) {
case CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW:
Expand Down Expand Up @@ -6014,6 +6032,7 @@ export {
isDM,
isSelfDM,
getWorkspaceChats,
getAllWorkspaceReports,
shouldDisableRename,
hasSingleParticipant,
getReportRecipientAccountIDs,
Expand Down Expand Up @@ -6072,6 +6091,7 @@ export {
hasUpdatedTotal,
isReportFieldDisabled,
getAvailableReportFields,
isReportOwner,
getReportFieldKey,
reportFieldsEnabled,
getAllAncestorReportActionIDs,
Expand All @@ -6093,6 +6113,7 @@ export {
hasActionsWithErrors,
getReportActionActorAccountID,
getGroupChatName,
canLeavePolicyExpenseChat,
getOutstandingChildRequest,
getVisibleChatMemberAccountIDs,
getParticipantAccountIDs,
Expand Down
2 changes: 2 additions & 0 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ function getOptionData({
: ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
result.alternateText += `${preposition} ${roomName}`;
}
} else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.LEAVE_POLICY) {
result.alternateText = Localize.translateLocal('workspace.invite.leftWorkspace');
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`;
} else {
Expand Down
115 changes: 112 additions & 3 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
EnablePolicyTagsParams,
EnablePolicyTaxesParams,
EnablePolicyWorkflowsParams,
LeavePolicyParams,
OpenDraftWorkspaceRequestParams,
OpenPolicyCategoriesPageParams,
OpenPolicyDistanceRatesPageParams,
Expand Down Expand Up @@ -774,7 +775,7 @@ function clearWorkspaceReimbursementErrors(policyID: string) {
/**
* Build optimistic data for removing users from the announcement room
*/
function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: number[]): AnnounceRoomMembersOnyxData {
function removeOptimisticAnnounceRoomMembers(policyID: string, policyName: string, accountIDs: number[]): AnnounceRoomMembersOnyxData {
const announceReport = ReportUtils.getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID);
const announceRoomMembers: AnnounceRoomMembersOnyxData = {
onyxOptimisticData: [],
Expand All @@ -794,14 +795,27 @@ function removeOptimisticAnnounceRoomMembers(policyID: string, accountIDs: numbe
key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`,
value: {
pendingChatMembers,
...(accountIDs.includes(sessionAccountID)
? {
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
oldPolicyName: policyName,
}
: {}),
},
});

announceRoomMembers.onyxFailureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`,
value: {
pendingChatMembers: announceReport?.pendingChatMembers ?? null,
...(accountIDs.includes(sessionAccountID)
? {
statusNum: announceReport.statusNum,
stateNum: announceReport.stateNum,
oldPolicyName: announceReport.oldPolicyName,
}
: {}),
},
});
announceRoomMembers.onyxSuccessData.push({
Expand Down Expand Up @@ -833,7 +847,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
const workspaceChats = ReportUtils.getWorkspaceChats(policyID, accountIDs);
const optimisticClosedReportActions = workspaceChats.map(() => ReportUtils.buildOptimisticClosedReportAction(sessionEmail, policy.name, CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY));

const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policyID, accountIDs);
const announceRoomMembers = removeOptimisticAnnounceRoomMembers(policy.id, policy.name, accountIDs);

const optimisticMembersState: OnyxCollection<PolicyMember> = {};
const successMembersState: OnyxCollection<PolicyMember> = {};
Expand Down Expand Up @@ -962,6 +976,100 @@ function removeMembers(accountIDs: number[], policyID: string) {
API.write(WRITE_COMMANDS.DELETE_MEMBERS_FROM_WORKSPACE, params, {optimisticData, successData, failureData});
}

function leaveWorkspace(policyID: string) {
const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}` as const;
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
const workspaceChats = ReportUtils.getAllWorkspaceReports(policyID);

const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: membersListKey,
value: {
[sessionAccountID]: {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
},
},
];
const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: membersListKey,
value: {
[sessionAccountID]: null,
},
},
];
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: membersListKey,
value: {
[sessionAccountID]: {
errors: ErrorUtils.getMicroSecondOnyxError('workspace.people.error.genericRemove'),
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
pendingAction: policy?.pendingAction,
},
},
];

const pendingChatMembers = ReportUtils.getPendingChatMembers([sessionAccountID], [], CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);

workspaceChats.forEach((report) => {
const parentReport = ReportUtils.getRootParentReport(report);
const reportToCheckOwner = isEmptyObject(parentReport) ? report : parentReport;

if (ReportUtils.isPolicyExpenseChat(report) && !ReportUtils.isReportOwner(reportToCheckOwner)) {
return;
}

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`,
value: {
statusNum: CONST.REPORT.STATUS_NUM.CLOSED,
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
oldPolicyName: policy?.name ?? '',
pendingChatMembers,
},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`,
value: {
pendingChatMembers: null,
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`,
value: {
pendingChatMembers: null,
},
});
});

const params: LeavePolicyParams = {
policyID,
email: sessionEmail,
};
API.write(WRITE_COMMANDS.LEAVE_POLICY, params, {optimisticData, successData, failureData});
}

function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newRole: typeof CONST.POLICY.ROLE.ADMIN | typeof CONST.POLICY.ROLE.USER) {
const previousPolicyMembers = {...allPolicyMembers};
const memberRoles: WorkspaceMembersRoleData[] = accountIDs.reduce((result: WorkspaceMembersRoleData[], accountID: number) => {
Expand Down Expand Up @@ -4982,6 +5090,7 @@ function setForeignCurrencyDefault(policyID: string, taxCode: string) {

export {
removeMembers,
leaveWorkspace,
updateWorkspaceMembersRole,
requestWorkspaceOwnerChange,
clearWorkspaceOwnerChangeFlow,
Expand Down
13 changes: 13 additions & 0 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2084,6 +2084,18 @@ function clearPolicyRoomNameErrors(reportID: string) {
});
}

/**
* @param reportID The reportID of the report.
*/
// eslint-disable-next-line rulesdir/no-negated-variables
function clearReportNotFoundErrors(reportID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {
errorFields: {
notFound: null,
},
});
}

function setIsComposerFullSize(reportID: string, isComposerFullSize: boolean) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, isComposerFullSize);
}
Expand Down Expand Up @@ -3202,6 +3214,7 @@ export {
toggleSubscribeToChildReport,
updatePolicyRoomNameAndNavigate,
clearPolicyRoomNameErrors,
clearReportNotFoundErrors,
clearIOUError,
subscribeToNewActionEvent,
notifyNewAction,
Expand Down
Loading

0 comments on commit 2ce785c

Please sign in to comment.