-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Add Task Title Validation on Main Composer Text Change #52941
Changes from 15 commits
9c825da
bd3a70e
bee1a66
faf4370
bcbde08
0e5325d
f90ce91
a3bd4bf
3d40a79
dd65326
cb37156
2c3b86d
708adbf
0c20987
5c51ae8
ed1b95d
867f77b
c3c0796
58bbcbe
36253a8
41c3215
3f08b65
dc57914
ad2272d
e638474
e63f27a
36e4c3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import debounce from 'lodash/debounce'; | ||
import {useCallback, useMemo, useState} from 'react'; | ||
import CONST from '@src/CONST'; | ||
|
||
const useHandleExceedMaxTaskTitleLength = () => { | ||
const [hasExceededMaxTitleLength, setHasExceededMaxTitleLength] = useState(false); | ||
|
||
const handleValueChange = useCallback((value: string) => { | ||
const match = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); | ||
if (match) { | ||
const title = match[3] ? match[3].trim().replace(/\n/g, ' ') : undefined; | ||
const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false; | ||
setHasExceededMaxTitleLength(exceeded); | ||
return true; | ||
} | ||
setHasExceededMaxTitleLength(false); | ||
return false; | ||
}, []); | ||
|
||
const validateTitleMaxLength = useMemo(() => debounce(handleValueChange, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true}), [handleValueChange]); | ||
|
||
return {hasExceededMaxTitleLength, validateTitleMaxLength}; | ||
}; | ||
|
||
export default useHandleExceedMaxTaskTitleLength; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -23,6 +23,7 @@ import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; | |||||||||||||||||||||||||||||||||
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; | ||||||||||||||||||||||||||||||||||
import useDebounce from '@hooks/useDebounce'; | ||||||||||||||||||||||||||||||||||
import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; | ||||||||||||||||||||||||||||||||||
import useHandleExceedMaxTaskTitleLength from '@hooks/useHandleExceedMaxTaskTitleLength'; | ||||||||||||||||||||||||||||||||||
import useLocalize from '@hooks/useLocalize'; | ||||||||||||||||||||||||||||||||||
import useNetwork from '@hooks/useNetwork'; | ||||||||||||||||||||||||||||||||||
import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||||||||||||||||||||||||||||||||||
|
@@ -172,6 +173,8 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
* Shows red borders and prevents the comment from being sent | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); | ||||||||||||||||||||||||||||||||||
const {hasExceededMaxTitleLength, validateTitleMaxLength} = useHandleExceedMaxTaskTitleLength(); | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Can we name it more specific to task title? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@hoangzinh In my opinion using a separate variable like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can move regrex matching out of useHandleExceedMaxTaskTitleLength hook. And add another wrapper debounce for validation methods like this. I think it works better. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoangzinh after test it might looks like this src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx const onValueChange = useCallback(
(value: string) => {
if (value.length === 0 && isComposerFullSize) {
Report.setIsComposerFullSize(reportID, false);
}
const taskCommentMatch = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION);
if (taskCommentMatch) {
const title = taskCommentMatch?.[3] ? taskCommentMatch[3].trim().replace(/\n/g, ' ') : '';
validateTaskTitleMaxLength(title);
} else {
validateCommentMaxLength(value, {reportID});
}
},
[isComposerFullSize, reportID, validateCommentMaxLength, validateTaskTitleMaxLength],
);
src/hooks/useHandleExceedMaxTaskTitleLength.ts const useHandleExceedMaxTaskTitleLength = () => {
const [hasExceededMaxTaskTitleLength, setHasExceededMaxTitleLength] = useState(false);
const handleValueChange = useCallback((title: string) => {
const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false;
setHasExceededMaxTitleLength(exceeded);
}, []);
const validateTaskTitleMaxLength = useMemo(() => debounce(handleValueChange, CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME, {leading: true}), [handleValueChange]);
return {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength};
};
proceed with the above change? or alternatively we can keep current There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you're right @wildan-m . Hmm, what do you think if we refactor it a bit:
So in main-composer, we will use 1500 as a debounce time for both plain function validations above. And in edit composer, we will have another debounce with 1500ms. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoangzinh if we're going to extract the debounce and not need faster debounce specific for task title, how about keeping the regex matching inside the hook? const useHandleExceedMaxTaskTitleLength = () => {
const [hasExceededMaxTaskTitleLength, setHasExceededMaxTitleLength] = useState(false);
const validateTaskTitleMaxLength = useCallback((value: string) => {
const match = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION);
if (match) {
const title = match[3] ? match[3].trim().replace(/\n/g, ' ') : undefined;
const exceeded = title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false;
setHasExceededMaxTitleLength(exceeded);
return true;
}
setHasExceededMaxTitleLength(false);
return false;
}, []);
return {hasExceededMaxTaskTitleLength, validateTaskTitleMaxLength};
};
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can @wildan-m, but it's kind of wrong in a case, let's say There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option we can consider refactoring existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoangzinh the code has been updated. I use and modify |
||||||||||||||||||||||||||||||||||
const [exceededMaxLength, setExceededMaxLength] = useState<number | null>(null); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const suggestionsRef = useRef<SuggestionsRef>(null); | ||||||||||||||||||||||||||||||||||
const composerRef = useRef<ComposerRef>(); | ||||||||||||||||||||||||||||||||||
|
@@ -306,6 +309,18 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
onComposerFocus?.(); | ||||||||||||||||||||||||||||||||||
}, [onComposerFocus]); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||
if (hasExceededMaxTitleLength) { | ||||||||||||||||||||||||||||||||||
setExceededMaxLength(CONST.TITLE_CHARACTER_LIMIT); | ||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
if (hasExceededMaxCommentLength) { | ||||||||||||||||||||||||||||||||||
setExceededMaxLength(CONST.MAX_COMMENT_LENGTH); | ||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
setExceededMaxLength(null); | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we can use nested if/else to read code easier here. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hoangzinh Although the lint check will pass, I believe we may prefer an early return? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We prefer an early return if it's only 1 if. For example if (true) {
// then do something
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I see, updated |
||||||||||||||||||||||||||||||||||
}, [hasExceededMaxTitleLength, hasExceededMaxCommentLength]); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// We are returning a callback here as we want to incoke the method on unmount only | ||||||||||||||||||||||||||||||||||
useEffect( | ||||||||||||||||||||||||||||||||||
() => () => { | ||||||||||||||||||||||||||||||||||
|
@@ -333,7 +348,7 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const hasReportRecipient = !isEmptyObject(reportRecipient); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; | ||||||||||||||||||||||||||||||||||
const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || !!exceededMaxLength; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value | ||||||||||||||||||||||||||||||||||
// useSharedValue on web doesn't support functions, so we need to wrap it in an object. | ||||||||||||||||||||||||||||||||||
|
@@ -399,9 +414,12 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
if (value.length === 0 && isComposerFullSize) { | ||||||||||||||||||||||||||||||||||
Report.setIsComposerFullSize(reportID, false); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
if (validateTitleMaxLength(value)) { | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's a debounce function, can we trust it to return true/false value here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://www.geeksforgeeks.org/lodash-_-debounce-method/ We set leading true, it will immediately executed at the initial attempt There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, I missed this comment. Hmm. I don't think it's a good idea to check conditions based on result of a debounce function. It's better if we detect comment format and use properly validation function here. For example: const isCreatingTaskComment = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION);
if (isCreatingTaskComment) {
validateTitleMaxLength(value)
} else {
validateCommentMaxLength(value, {reportID});
} what do you think? |
||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
validateCommentMaxLength(value, {reportID}); | ||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||
[isComposerFullSize, reportID, validateCommentMaxLength], | ||||||||||||||||||||||||||||||||||
[isComposerFullSize, reportID, validateCommentMaxLength, validateTitleMaxLength], | ||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||
|
@@ -436,15 +454,15 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
styles.flexRow, | ||||||||||||||||||||||||||||||||||
styles.chatItemComposeBox, | ||||||||||||||||||||||||||||||||||
isComposerFullSize && styles.chatItemFullComposeBox, | ||||||||||||||||||||||||||||||||||
hasExceededMaxCommentLength && styles.borderColorDanger, | ||||||||||||||||||||||||||||||||||
!!exceededMaxLength && styles.borderColorDanger, | ||||||||||||||||||||||||||||||||||
]} | ||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||
<AttachmentModal | ||||||||||||||||||||||||||||||||||
headerTitle={translate('reportActionCompose.sendAttachment')} | ||||||||||||||||||||||||||||||||||
onConfirm={addAttachment} | ||||||||||||||||||||||||||||||||||
onModalShow={() => setIsAttachmentPreviewActive(true)} | ||||||||||||||||||||||||||||||||||
onModalHide={onAttachmentPreviewClose} | ||||||||||||||||||||||||||||||||||
shouldDisableSendButton={hasExceededMaxCommentLength} | ||||||||||||||||||||||||||||||||||
shouldDisableSendButton={!!exceededMaxLength} | ||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||
{({displayFileInModal}) => ( | ||||||||||||||||||||||||||||||||||
<> | ||||||||||||||||||||||||||||||||||
|
@@ -463,7 +481,7 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
onAddActionPressed={onAddActionPressed} | ||||||||||||||||||||||||||||||||||
onItemSelected={onItemSelected} | ||||||||||||||||||||||||||||||||||
actionButtonRef={actionButtonRef} | ||||||||||||||||||||||||||||||||||
shouldDisableAttachmentItem={hasExceededMaxCommentLength} | ||||||||||||||||||||||||||||||||||
shouldDisableAttachmentItem={!!exceededMaxLength} | ||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||
<ComposerWithSuggestions | ||||||||||||||||||||||||||||||||||
ref={(ref) => { | ||||||||||||||||||||||||||||||||||
|
@@ -549,7 +567,12 @@ function ReportActionCompose({ | |||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||
{!shouldUseNarrowLayout && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />} | ||||||||||||||||||||||||||||||||||
<ReportTypingIndicator reportID={reportID} /> | ||||||||||||||||||||||||||||||||||
{hasExceededMaxCommentLength && <ExceededCommentLength />} | ||||||||||||||||||||||||||||||||||
{!!exceededMaxLength && ( | ||||||||||||||||||||||||||||||||||
<ExceededCommentLength | ||||||||||||||||||||||||||||||||||
maxCommentLength={exceededMaxLength} | ||||||||||||||||||||||||||||||||||
isTaskTitle={hasExceededMaxTitleLength} | ||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||
</View> | ||||||||||||||||||||||||||||||||||
</OfflineWithFeedback> | ||||||||||||||||||||||||||||||||||
{!isSmallScreenWidth && ( | ||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This caused an issue
We should use
EMAIL_WITH_OPTIONAL_DOMAIN.source
here