Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS migration] Migrate 'WorkspaceInvite' page to TypeScript #35080

Merged
merged 17 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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: 2 additions & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,8 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: Record<string, number>;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDsDraft | undefined;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string | undefined;
VickyStash marked this conversation as resolved.
Show resolved Hide resolved
[ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report;
[ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata;
[ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions;
Expand Down
5 changes: 4 additions & 1 deletion src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type User = {
login?: string;

/** Element to show on the right side of the item */
rightElement?: ReactElement;
rightElement?: ReactElement | null;
VickyStash marked this conversation as resolved.
Show resolved Hide resolved

/** Icons for the user (can be multiple if it's a Workspace) */
icons?: Icon[];
Expand Down Expand Up @@ -129,6 +129,9 @@ type Section<TItem extends User | RadioItem> = {

/** Whether this section items disabled for selection */
isDisabled?: boolean;

/** Whether this section should be shown or not */
shouldShow?: boolean;
};

type BaseSelectionListProps<TItem extends User | RadioItem> = Partial<ChildrenProps> & {
Expand Down
27 changes: 15 additions & 12 deletions src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import lodashGet from 'lodash/get';
import lodashOrderBy from 'lodash/orderBy';
import lodashSet from 'lodash/set';
import lodashSortBy from 'lodash/sortBy';
import type {ReactElement} from 'react';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
Expand All @@ -15,7 +16,6 @@ import type {Participant} from '@src/types/onyx/IOU';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import times from '@src/utils/times';
import Timing from './actions/Timing';
Expand Down Expand Up @@ -103,13 +103,13 @@ type GetOptionsConfig = {

type MemberForList = {
text: string;
alternateText: string | null;
keyForList: string | null;
alternateText: string;
keyForList: string;
isSelected: boolean;
isDisabled: boolean | null;
accountID?: number | null;
login: string | null;
rightElement: React.ReactNode | null;
isDisabled: boolean;
accountID?: number;
login: string;
rightElement: ReactElement | null;
icons?: OnyxCommon.Icon[];
pendingAction?: OnyxCommon.PendingAction;
};
Expand Down Expand Up @@ -310,9 +310,9 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, perso
/**
* Return true if personal details data is ready, i.e. report list options can be created.
*/
function isPersonalDetailsReady(personalDetails: OnyxEntry<PersonalDetailsList>): boolean {
const personalDetailsKeys = Object.keys(personalDetails ?? {});
return personalDetailsKeys.some((key) => personalDetails?.[key]?.accountID);
function isPersonalDetailsReady(personalDetails: OnyxEntry<PersonalDetailsList> | ReportUtils.OptionData[]): boolean {
const personalDetailsValues = Array.isArray(personalDetails) ? personalDetails : Object.values(personalDetails ?? {});
return personalDetailsValues.some((personalDetail) => personalDetail?.accountID);
}
VickyStash marked this conversation as resolved.
Show resolved Hide resolved

/**
Expand Down Expand Up @@ -1819,12 +1819,14 @@ function getShareDestinationOptions(
* @param member - personalDetails or userToInvite
* @param config - keys to overwrite the default values
*/
function formatMemberForList(member: ReportUtils.OptionData, config: ReportUtils.OptionData | EmptyObject = {}): MemberForList | undefined {
function formatMemberForList(member: ReportUtils.OptionData, config?: Partial<MemberForList>): MemberForList;
function formatMemberForList(member: null | undefined, config?: Partial<MemberForList>): undefined;
function formatMemberForList(member: ReportUtils.OptionData | null | undefined, config: Partial<MemberForList> = {}): MemberForList | undefined {
if (!member) {
return undefined;
}

const accountID = member.accountID;
const accountID = member.accountID ?? undefined;

return {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
Expand Down Expand Up @@ -2003,3 +2005,4 @@ export {
formatSectionsFromSearchTerm,
transformedTaxRates,
};
export type {MemberForList};
VickyStash marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 3 additions & 3 deletions src/pages/RoomInvitePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function RoomInvitePage(props) {

// Update selectedOptions with the latest personalDetails information
const detailsMap = {};
_.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail, false)));
_.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
const newSelectedOptions = [];
_.forEach(selectedOptions, (option) => {
newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
Expand Down Expand Up @@ -142,7 +142,7 @@ function RoomInvitePage(props) {
// Filtering out selected users from the search results
const selectedLogins = _.map(selectedOptions, ({login}) => login);
const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login));
const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false));
const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail));
const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login);

sectionsArr.push({
Expand All @@ -156,7 +156,7 @@ function RoomInvitePage(props) {
if (hasUnselectedUserToInvite) {
sectionsArr.push({
title: undefined,
data: [OptionsListUtils.formatMemberForList(userToInvite, false)],
data: [OptionsListUtils.formatMemberForList(userToInvite)],
shouldShow: true,
indexOffset,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,139 +1,119 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import type {StackScreenProps} from '@react-navigation/stack';
import lodashDebounce from 'lodash/debounce';
import React, {useEffect, useState} from 'react';
import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import type {OnyxEntry} from 'react-native-onyx';
import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import MultipleAvatars from '@components/MultipleAvatars';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import type {AnimatedTextInputRef} from '@components/RNTextInput';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withNavigationFocus from '@components/withNavigationFocus';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import type {SettingsNavigatorParamList} from '@navigation/types';
import * as Link from '@userActions/Link';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {InvitedEmailsToAccountIDsDraft, PersonalDetailsList} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import SearchInputManager from './SearchInputManager';
import {policyDefaultProps, policyPropTypes} from './withPolicy';
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';

const personalDetailsPropTypes = PropTypes.shape({
/** The accountID of the person */
accountID: PropTypes.number.isRequired,

/** The login of the person (either email or phone number) */
login: PropTypes.string,

/** The URL of the person's avatar (there should already be a default avatar if
the person doesn't have their own avatar uploaded yet, except for anon users) */
avatar: PropTypes.string,

/** This is either the user's full name, or their login if full name is an empty string */
displayName: PropTypes.string,
});

const propTypes = {
type WorkspaceInviteMessagePageOnyxProps = {
/** All of the personal details for everyone */
allPersonalDetails: PropTypes.objectOf(personalDetailsPropTypes),

invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number),
allPersonalDetails: OnyxEntry<PersonalDetailsList>;

/** URL Route params */
route: PropTypes.shape({
/** Params from the URL path */
params: PropTypes.shape({
/** policyID passed via route: /workspace/:policyID/invite-message */
policyID: PropTypes.string,
}),
}).isRequired,
/** An object containing the accountID for every invited user email */
invitedEmailsToAccountIDsDraft: OnyxEntry<InvitedEmailsToAccountIDsDraft | undefined>;

...policyPropTypes,
/** Updated workspace invite message */
workspaceInviteMessageDraft: OnyxEntry<string | undefined>;
};

const defaultProps = {
...policyDefaultProps,
allPersonalDetails: {},
invitedEmailsToAccountIDsDraft: {},
};
type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps &
WorkspaceInviteMessagePageOnyxProps &
StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.INVITE_MESSAGE>;

function WorkspaceInviteMessagePage(props) {
function WorkspaceInviteMessagePage({workspaceInviteMessageDraft, invitedEmailsToAccountIDsDraft, policy, route, allPersonalDetails}: WorkspaceInviteMessagePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const [welcomeNote, setWelcomeNote] = useState();
const [welcomeNote, setWelcomeNote] = useState<string>();

const {inputCallbackRef} = useAutoFocusInput();

const getDefaultWelcomeNote = () =>
props.workspaceInviteMessageDraft ||
// workspaceInviteMessageDraft can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
workspaceInviteMessageDraft ||
translate('workspace.inviteMessage.welcomeNote', {
Comment on lines +64 to +65
Copy link
Contributor

@allgandalf allgandalf Nov 14, 2024

Choose a reason for hiding this comment

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

@VickyStash , why did you prefer a OR || operator here instead of nullish ?? and silenced the type error ? any special reason ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently reviewing proposal for the issue #51655, where a contributor suggested to use nullish operator ?? instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it was to keep the logic consistent with the way it was before the TS migration.
As you can see props.workspaceInviteMessageDraft || was used, and since workspaceInviteMessageDraft can be the empty string the change to ?? would affect the logic before the TS migration, we wanted to escape any side updates when it's changes the logic.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh is it, thanks for the info 🙇

workspaceName: props.policy.name,
workspaceName: policy?.name ?? '',
});

useEffect(() => {
if (!_.isEmpty(props.invitedEmailsToAccountIDsDraft)) {
if (!isEmptyObject(invitedEmailsToAccountIDsDraft)) {
setWelcomeNote(getDefaultWelcomeNote());
return;
}
Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID), true);
Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const debouncedSaveDraft = _.debounce((newDraft) => {
Policy.setWorkspaceInviteMessageDraft(props.route.params.policyID, newDraft);
const debouncedSaveDraft = lodashDebounce((newDraft: string) => {
Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft);
});

const sendInvitation = () => {
Keyboard.dismiss();
Policy.addMembersToWorkspace(props.invitedEmailsToAccountIDsDraft, welcomeNote, props.route.params.policyID);
Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {});
Policy.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, welcomeNote ?? '', route.params.policyID);
Policy.setWorkspaceInviteMembersDraft(route.params.policyID, {});
SearchInputManager.searchInput = '';
// Pop the invite message page before navigating to the members page.
Navigation.goBack(ROUTES.HOME);
Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID));
Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(route.params.policyID));
};

/**
* Opens privacy url as an external link
* @param {Object} event
*/
const openPrivacyURL = (event) => {
event.preventDefault();
/** Opens privacy url as an external link */
const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => {
event?.preventDefault();
Link.openExternalLink(CONST.PRIVACY_URL);
};

const validate = () => {
const errorFields = {};
if (_.isEmpty(props.invitedEmailsToAccountIDsDraft)) {
const validate = (): Errors => {
const errorFields: Errors = {};
if (isEmptyObject(invitedEmailsToAccountIDsDraft)) {
errorFields.welcomeMessage = 'workspace.inviteMessage.inviteNoMembersError';
}
return errorFields;
};

const policyName = lodashGet(props.policy, 'name');
const policyName = policy?.name;

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={WorkspaceInviteMessagePage.displayName}
>
<FullPageNotFoundView
shouldShow={_.isEmpty(props.policy) || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy)}
subtitleKey={_.isEmpty(props.policy) ? undefined : 'workspace.common.notAuthorized'}
shouldShow={isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)}
subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
>
<HeaderWithBackButton
Expand All @@ -143,7 +123,7 @@ function WorkspaceInviteMessagePage(props) {
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
shouldShowBackButton
onCloseButtonPress={() => Navigation.dismissModal()}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID))}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID))}
/>
<FormProvider
style={[styles.flexGrow1, styles.ph5]}
Expand All @@ -169,7 +149,11 @@ function WorkspaceInviteMessagePage(props) {
<View style={[styles.mv4, styles.justifyContentCenter, styles.alignItemsCenter]}>
<MultipleAvatars
size={CONST.AVATAR_SIZE.LARGE}
icons={OptionsListUtils.getAvatarsForAccountIDs(_.values(props.invitedEmailsToAccountIDsDraft), props.allPersonalDetails, props.invitedEmailsToAccountIDsDraft)}
icons={OptionsListUtils.getAvatarsForAccountIDs(
Object.values(invitedEmailsToAccountIDsDraft ?? {}),
allPersonalDetails ?? {},
invitedEmailsToAccountIDsDraft ?? {},
)}
shouldStackHorizontally
shouldDisplayAvatarsInRows
secondAvatarStyle={[styles.secondAvatarInline]}
Expand All @@ -191,16 +175,16 @@ function WorkspaceInviteMessagePage(props) {
containerStyles={[styles.autoGrowHeightMultilineInput]}
defaultValue={getDefaultWelcomeNote()}
value={welcomeNote}
onChangeText={(text) => {
onChangeText={(text: string) => {
setWelcomeNote(text);
debouncedSaveDraft(text);
}}
ref={(el) => {
if (!el) {
ref={(element: AnimatedTextInputRef) => {
if (!element) {
return;
}
inputCallbackRef(el);
updateMultilineInputRange(el);
inputCallbackRef(element);
updateMultilineInputRange(element);
}}
/>
</View>
Expand All @@ -210,13 +194,10 @@ function WorkspaceInviteMessagePage(props) {
);
}

WorkspaceInviteMessagePage.propTypes = propTypes;
WorkspaceInviteMessagePage.defaultProps = defaultProps;
WorkspaceInviteMessagePage.displayName = 'WorkspaceInviteMessagePage';

export default compose(
withPolicyAndFullscreenLoading,
withOnyx({
export default withPolicyAndFullscreenLoading(
withOnyx<WorkspaceInviteMessagePageProps, WorkspaceInviteMessagePageOnyxProps>({
allPersonalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
Expand All @@ -226,6 +207,5 @@ export default compose(
workspaceInviteMessageDraft: {
key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${route.params.policyID.toString()}`,
},
}),
withNavigationFocus,
)(WorkspaceInviteMessagePage);
})(WorkspaceInviteMessagePage),
);
Loading
Loading