mattermost-mobile/app/screens/edit_profile/components/form.tsx
Daniel Espino García 0c4a42a06a
Remove tand prevent double tap (#9078)
* Remove the t function and all wrong uses of preventDoubleTap

* Fix existing typo

* Fix tests

* Address comments

* Fix test
2025-08-25 12:03:01 +02:00

341 lines
12 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {defineMessages, type MessageDescriptor, useIntl} from 'react-intl';
import {Keyboard, StyleSheet, View} from 'react-native';
import {useTheme} from '@context/theme';
import useFieldRefs from '@hooks/field_refs';
import {getErrorMessage} from '@utils/errors';
import {logError} from '@utils/log';
import {sortCustomProfileAttributes, formatOptionsForSelector, isCustomFieldSamlLinked} from '@utils/user';
import DisabledFields from './disabled_fields';
import EmailField from './email_field';
import Field from './field';
import SelectField from './select_field';
import type {CustomProfileFieldModel} from '@database/models/server';
import type UserModel from '@typings/database/models/servers/user';
import type {FieldConfig, FieldSequence, UserInfo} from '@typings/screens/edit_profile';
type Props = {
canSave: boolean;
currentUser: UserModel;
isTablet?: boolean;
lockedFirstName: boolean;
lockedLastName: boolean;
lockedNickname: boolean;
lockedPosition: boolean;
onUpdateField: (fieldKey: string, name: string) => void;
error?: unknown;
userInfo: UserInfo;
submitUser: () => void;
enableCustomAttributes?: boolean;
customFields?: CustomProfileFieldModel[];
}
const includesSsoService = (sso: string) => ['gitlab', 'google', 'office365'].includes(sso);
const isSAMLOrLDAP = (protocol: string) => ['ldap', 'saml'].includes(protocol);
const FIELDS: {[id: string]: MessageDescriptor} = defineMessages({
firstName: {
id: 'user.settings.general.firstName',
defaultMessage: 'First Name',
},
lastName: {
id: 'user.settings.general.lastName',
defaultMessage: 'Last Name',
},
username: {
id: 'user.settings.general.username',
defaultMessage: 'Username',
},
nickname: {
id: 'user.settings.general.nickname',
defaultMessage: 'Nickname',
},
position: {
id: 'user.settings.general.position',
defaultMessage: 'Position',
},
email: {
id: 'user.settings.general.email',
defaultMessage: 'Email',
},
});
const styles = StyleSheet.create({
footer: {
height: 40,
width: '100%',
},
separator: {
height: 15,
},
});
export const CUSTOM_ATTRS_PREFIX = 'customAttributes';
const FIRST_NAME_FIELD = 'firstName';
const LAST_NAME_FIELD = 'lastName';
const USERNAME_FIELD = 'username';
const EMAIL_FIELD = 'email';
const NICKNAME_FIELD = 'nickname';
const POSITION_FIELD = 'position';
const profileKeys = [FIRST_NAME_FIELD, LAST_NAME_FIELD, USERNAME_FIELD, EMAIL_FIELD, NICKNAME_FIELD, POSITION_FIELD];
export const getFieldKey = (key: string) => `${CUSTOM_ATTRS_PREFIX}.${key}`;
const ProfileForm = ({
canSave, currentUser, isTablet,
lockedFirstName, lockedLastName, lockedNickname, lockedPosition,
onUpdateField, userInfo, submitUser, error, enableCustomAttributes, customFields,
}: Props) => {
const theme = useTheme();
const intl = useIntl();
const [getRef, setRef] = useFieldRefs();
const {formatMessage} = intl;
const errorMessage = error == null ? undefined : getErrorMessage(error, intl) as string;
const totalCustomAttrs = useMemo(() => (
enableCustomAttributes ? Object.keys(userInfo.customAttributes).length : 0
), [enableCustomAttributes, userInfo.customAttributes]);
const formKeys = useMemo(() => {
const newKeys = Object.keys(userInfo.customAttributes).sort(
(a: string, b: string): number => {
return sortCustomProfileAttributes(userInfo.customAttributes[a], userInfo.customAttributes[b]);
}).map((k) => getFieldKey(k));
return totalCustomAttrs === 0 ? profileKeys : [...profileKeys, ...newKeys];
}, [userInfo.customAttributes, totalCustomAttrs]);
// Create a map of field definitions for quick lookup
const customFieldsMap = useMemo(() => {
const map = new Map<string, CustomProfileFieldModel>();
customFields?.forEach((field) => {
map.set(field.id, field);
});
return map;
}, [customFields]);
const userProfileFields: FieldSequence = useMemo(() => {
const service = currentUser.authService;
const fields: FieldSequence = {};
formKeys.forEach((element) => {
switch (element) {
case FIRST_NAME_FIELD:
fields[FIRST_NAME_FIELD] = {
isDisabled: (isSAMLOrLDAP(service) && lockedFirstName) || includesSsoService(service),
};
break;
case LAST_NAME_FIELD:
fields[LAST_NAME_FIELD] = {
isDisabled: (isSAMLOrLDAP(service) && lockedLastName) || includesSsoService(service),
};
break;
case USERNAME_FIELD:
fields[USERNAME_FIELD] = {
isDisabled: service !== '',
maxLength: 22,
error: errorMessage,
};
break;
case EMAIL_FIELD:
fields[EMAIL_FIELD] = {
isDisabled: true,
};
break;
case NICKNAME_FIELD:
fields[NICKNAME_FIELD] = {
isDisabled: isSAMLOrLDAP(service) && lockedNickname,
maxLength: 64,
};
break;
case POSITION_FIELD:
fields[POSITION_FIELD] = {
isDisabled: isSAMLOrLDAP(service) && lockedPosition,
maxLength: 128,
};
break;
default:
fields[element] = {
isDisabled: false,
maxLength: 64,
};
}
});
// Handle custom attributes - check if SAML linked
Object.keys(userInfo.customAttributes).forEach((key) => {
const customField = customFieldsMap.get(key);
const fieldKey = getFieldKey(key);
if (customField && fields[fieldKey]) {
fields[fieldKey].isDisabled = isCustomFieldSamlLinked(customField);
}
});
return fields;
}, [lockedFirstName, lockedLastName, lockedNickname, lockedPosition, currentUser.authService, formKeys, errorMessage, customFieldsMap, userInfo.customAttributes]);
const onFocusNextField = useCallback(((fieldKey: string) => {
const findNextField = () => {
const fields = Object.keys(userProfileFields);
const curIndex = fields.indexOf(fieldKey);
const searchIndex = curIndex + 1;
if (curIndex === -1 || searchIndex > fields.length) {
return undefined;
}
const remainingFields = fields.slice(searchIndex);
const nextFieldIndex = remainingFields.findIndex((f: string) => {
const field = userProfileFields[f];
return !field.isDisabled;
});
if (nextFieldIndex === -1) {
return {isLastEnabledField: true, nextField: undefined};
}
const fieldName = remainingFields[nextFieldIndex];
return {isLastEnabledField: false, nextField: fieldName};
};
const next = findNextField();
if (next?.isLastEnabledField && canSave) {
// performs form submission
Keyboard.dismiss();
submitUser();
} else if (next?.nextField) {
getRef(next?.nextField)?.focus();
} else {
Keyboard.dismiss();
}
}), [canSave, userProfileFields, submitUser, getRef]);
const hasDisabledFields = Object.values(userProfileFields).filter((field) => field.isDisabled).length > 0;
const fieldConfig: FieldConfig = {
blurOnSubmit: false,
enablesReturnKeyAutomatically: true,
onFocusNextField,
onTextChange: onUpdateField,
returnKeyType: 'next',
};
const getFieldID = (key: string) => key.slice(CUSTOM_ATTRS_PREFIX.length + 1);
const getValue = (key: string): string => {
const val = userInfo[key as keyof UserInfo];
if (typeof val === 'string') {
return val;
}
try {
const customKey = getFieldID(key);
return userInfo.customAttributes[customKey].value;
} catch {
logError('Attempted to access unknown user property: ', key);
return '';
}
};
const renderStandardAttribute = (key: string, notLast: boolean) => (
<Field
{...fieldConfig}
fieldKey={key}
fieldRef={setRef(key)}
isDisabled={userProfileFields[key].isDisabled}
label={formatMessage(FIELDS[key])}
testID={`edit_profile_form.${key}`}
value={getValue(key)}
maxLength={userProfileFields[key].maxLength}
returnKeyType={notLast ? 'next' : 'done'}
error={userProfileFields[key].error}
/>);
const renderEmailAttribute = () => (userInfo.email &&
<EmailField
authService={currentUser.authService}
isDisabled={userProfileFields.email.isDisabled}
email={userInfo.email}
label={formatMessage(FIELDS.email)}
fieldRef={setRef(EMAIL_FIELD)}
onChange={onUpdateField}
onFocusNextField={onFocusNextField}
theme={theme}
isTablet={Boolean(isTablet)}
/>);
const renderCustomAttribute = (key: string, notLast: boolean) => {
const fieldID = getFieldID(key);
const customAttribute = userInfo.customAttributes[fieldID];
const fieldDefinition = customFieldsMap.get(fieldID);
// Check if this is a select or multiselect field
if (fieldDefinition && (fieldDefinition.type === 'select' || fieldDefinition.type === 'multiselect')) {
const options = formatOptionsForSelector(fieldDefinition);
return (
<SelectField
fieldKey={key}
label={customAttribute.name}
value={getValue(key)}
options={options}
isDisabled={userProfileFields[key].isDisabled}
onValueChange={onUpdateField}
onFocusNextField={onFocusNextField}
testID={`edit_profile_form.${key}`}
isMultiselect={fieldDefinition.type === 'multiselect'}
/>
);
}
// Default to text field for other types
return (
<Field
fieldKey={key}
isDisabled={userProfileFields[key].isDisabled}
fieldRef={setRef(key)}
label={customAttribute.name}
maxLength={128}
testID={`edit_profile_form.${key}`}
{...fieldConfig}
returnKeyType={notLast ? 'next' : 'done'}
value={getValue(key)}
/>);
};
const renderAttribute = (key: string, notLast: boolean) => {
if (key.startsWith(CUSTOM_ATTRS_PREFIX)) {
return renderCustomAttribute(key, notLast);
}
if (key === EMAIL_FIELD) {
return renderEmailAttribute();
}
return renderStandardAttribute(key, notLast);
};
const renderAllAttributes = formKeys.map((key, index) => {
const notLast = index < (formKeys.length - 1);
return (
<View key={key}>
{renderAttribute(key, notLast)}
{notLast && <View style={styles.separator}/>}
</View>);
});
return (
<>
{hasDisabledFields && <DisabledFields isTablet={isTablet}/>}
{renderAllAttributes}
<View style={styles.footer}/>
</>
);
};
export default ProfileForm;