-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Fix for Not found page appearing for private notes list, view and edit pages in deep link #29636
Conversation
CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅ |
I have read the CLA Document and I hereby sign the CLA |
recheck |
@ArekChr What do you think about this pull request? |
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.
overall looks good, left some comments
function WithReportAndPrivateNotesOrNotFound(props) { | ||
const {route, report, network, session} = props; |
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.
function WithReportAndPrivateNotesOrNotFound(props) { | |
const {route, report, network, session} = props; | |
function WithReportAndPrivateNotesOrNotFound({route, report, network, session}) { |
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.
@ArekChr As this is HOC, I am passing the all the props to WrapperComponent so I need all of them:
const rest = _.omit(props, ['forwardedRef']);
return (
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
ref={props.forwardedRef}
/>
);
So I can't move const {route, report, network, session} = props;
in component arguments.
What do you think?
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.
Now I understand your point of view. It makes sense. But it could be slightly improved, what do you think about spreading rest props here instead?
function WithReportAndPrivateNotesOrNotFound({ forwardedRef , ...props}) {
Then we will no need to use underscore omit and pass props with spread inside WrapperComponent
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.
@ArekChr This is a good idea in general but if I do it, what should i replace {...rest}
with? I want to pass all the properties to WrappedComponent as props (except forwardedRef). As we already spreaded props, I can't pass it to WrappedComponent anymore. Is there a solution for this?
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
ref={forwardedRef}
/>
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.
You will replace it with {...props}
, or you can name it {...rest}
instead {...props}
for better readability, the idea is to omit and forwardedRef
from rest props by not using underscore.
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.
@ArekChr I see what you are saying.
To omit underscore usage, here is what I can do based on your suggestion:
Option 1
Changing to:
function WithReportAndPrivateNotesOrNotFound({ forwardedRef , ...rest}) {
.
.
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
ref={forwardedRef}
/>
In this case, I need to replace all the route
usage with rest.route
. I need to do the same with report, network and session objects. This is because route
is not defined now. If I do this, its not that readable.
Option 2:
Changing to:
function WithReportAndPrivateNotesOrNotFound({ forwardedRef , ...props}) {
.
.
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={forwardedRef}
/>
This is same as above. Better than option 1 in readability but I am repeating the props.
everywhere now to access route, report, network and session. To avoid this, I originally used const {route, report, network, session} = props;
. But I can go with this option if this is what you suggest is better.
Option 3:
Changing to:
function WithReportAndPrivateNotesOrNotFound({ forwardedRef, route, report, network, session, ...props}) {
.
.
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{route, report, network, session, ...props}
ref={forwardedRef}
/>
In this case, I am getting route, report, network, session
in input arg and then getting it constructing it back in WrappedComponent.
I see these 3 options to not use underscore and use route, report, network, session
for my usecase and also pass it them to WrappedComponent along with other props. Should I go with Option 2?
By the way, to keep in mind for future usecases, can you tell me why we are avoiding _.omit
? Should we skip it in general? Is it because it creates a shallow copy (still a copy) of the object?
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.
@salonikumawat28 Option 2 looks good to me. We can it combine with your current approach so you can destruct in props and below the props as it is now.
So it will look like this:
function WithReportAndPrivateNotesOrNotFound({ forwardedRef , ...props}) {
const {route, report, network, session} = props;
...
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{ ...props }
ref={forwardedRef}
/>
Regarding underscore, this lib is removed while TypeScript migration so we will not use this anymore in future.
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.
Made the changes. Thanks.
const shouldShowFullScreenLoadingIndicator = isLoadingPrivateNotes !== false && isPrivateNotesEmpty; | ||
|
||
// eslint-disable-next-line rulesdir/no-negated-variables | ||
const shouldShowNotFoundPage = useMemo(() => { |
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.
Why this value is memoized?
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.
@ArekChr I am calling ReportUtils.isArchivedRoom(report)
. In addition, I am doing conditional checks in the memoized function. I dont want this to be re-computed every time until deps change.
import ONYXKEYS from '../../../ONYXKEYS'; | ||
|
||
export default function (WrappedComponent) { | ||
const propTypes = { |
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.
Move propTypes and default props outside of this function
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.
@ArekChr There are 2 functions in HOC. One is outside function representing HOC and another function within representing enhanced wrapper component. propTypes
and defaultProps
are already outside the enhanced component but they are within the HOC function.
i.e.
i.e. its outside function defined at line 55-56 but inside function defined at line 18-19.
This is similar to other HOC like withReportOrNotFound
and withReportAndReportActionOrNotFound
. Example snippet of withReportOrNotFound
:
App/src/pages/home/report/withReportAndReportActionOrNotFound.js
Lines 18 to 80 in 651be71
export default function (WrappedComponent) { | |
const propTypes = { | |
/** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component. | |
* That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */ | |
forwardedRef: PropTypes.func, | |
/** The report currently being looked at */ | |
report: reportPropTypes, | |
/** The report metadata */ | |
reportMetadata: reportMetadataPropTypes, | |
/** Array of report actions for this report */ | |
reportActions: PropTypes.shape(reportActionPropTypes), | |
/** The policies which the user has access to */ | |
policies: PropTypes.objectOf( | |
PropTypes.shape({ | |
/** The policy name */ | |
name: PropTypes.string, | |
/** The type of the policy */ | |
type: PropTypes.string, | |
}), | |
), | |
/** Route params */ | |
route: PropTypes.shape({ | |
params: PropTypes.shape({ | |
/** Report ID passed via route */ | |
reportID: PropTypes.string, | |
/** ReportActionID passed via route */ | |
reportActionID: PropTypes.string, | |
}), | |
}).isRequired, | |
/** Beta features list */ | |
betas: PropTypes.arrayOf(PropTypes.string), | |
/** Indicated whether the report data is loading */ | |
isLoadingReportData: PropTypes.bool, | |
/** Is the window width narrow, like on a mobile device? */ | |
isSmallScreenWidth: PropTypes.bool.isRequired, | |
}; | |
const defaultProps = { | |
forwardedRef: () => {}, | |
reportActions: {}, | |
report: {}, | |
reportMetadata: { | |
isLoadingReportActions: false, | |
isLoadingMoreReportActions: false, | |
}, | |
policies: {}, | |
betas: [], | |
isLoadingReportData: true, | |
}; | |
// eslint-disable-next-line rulesdir/no-negated-variables | |
function WithReportAndReportActionOrNotFound(props) { | |
const getReportAction = useCallback(() => { |
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.
I see we have 2 different approaches in the codebase. However, If you define propTypes and defaultProps inside the component, they'll be redefined every time the component is invoked, which is not efficient.
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.
@ArekChr Sorry for the confusion. I want to ask further clarification. Is your proposal is to move the propTypes and defaultProps inside the component or outside the component?
In first comment, you mentioned moving it outside of the function:
Move propTypes and default props outside of this function
In new comment, you mentioned that defining the propTypes and defaultProps outside the component makes it inefficient as props are redefined every time the component is invoked.
I see we have 2 different approaches in the codebase. However, If you define propTypes and defaultProps outside the component, they'll be redefined every time the component is invoked, which is not efficient.
My understanding is that we should not define the propTypes and defaultProps inside component because they will be redefined every time the component is invoked. Is this understanding not correct?
Assuming my understanding is correct, I am defining the propTypes and defaultProps outside the React component. In below example, the component is defined at line 55. The function defined in line 18 is not React component, it's the HOC function. The propTypes and defaultProps are outside React component where the component is defined in line 55.
Let me know if this explanation and understanding correct?
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.
Sorry for the confusion, I had a typo in the previous message, I mean inside, if we define propTypes inside HOC these props will be recreated each time per instance, but this is just a little thing, props are not heavy, so we can leave as it is.
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.
@ArekChr Thanks for the clarifications. I understand your point now. I have moved it outside.
Reviewer Checklist
Screenshots/VideosWebweb.movMobile Web - Chromemweb.chrome.movMobile Web - Safarimweb.safari.movDesktopdesktop.moviOSios.movios2.movAndroidandroid.mov |
@salonikumawat28 I found a bug. When navigating to private notes, a "not found" page is displayed in case I don't have any private notes. Nagranie.z.ekranu.2023-10-19.o.09.08.12.mov |
@ArekChr Thanks for catching the issue. I have fixed this and tested following scenarios again:
|
@salonikumawat28 Thanks for update 🙌🏼 Testing |
@salonikumawat28, I found another bug but it appears to be unrelated to the current issue. When I navigate to http://localhost:8082/r/:workspaceid/notes/:accountid/edit to edit private notes and then go back, the workspaceId changes to 'undefined'. I've observed this behaviour on both mweb (Safari/Chrome) and the web. I also verified that it happens on staging. If this isn't a quick fix, perhaps we should open a separate issue. Overall I tested all platforms and the current issue is fixed and works as expected 🙌🏼 mweb.safari.mov |
<WrappedComponent | ||
// eslint-disable-next-line react/jsx-props-no-spreading | ||
{...props} | ||
ref={props.forwardedRef} |
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.
forwardedRef is destructed in line 55, it no longer exists in props.
ref={props.forwardedRef} | |
ref={forwardedRef} |
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.
@ArekChr my bad. Fixed. thanks.
@ArekChr I also checked that this issue is also present in staging. I suggest to take this issue separately as it's not similar to existing issue. What do you think? Once the PR merges, I can create this issue if that's fine with you. @ArekChr If PR looks good to you, this can be approved now? |
@ArekChr If everything looks good to you, can this PR be merged to avoid penalty? 6 business days penalty is approaching in few hours on this PR. |
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.
All good, Thanks for PR!
@MonilBhavsar Can you have a look at the PR if it looks good? |
} | ||
|
||
Report.getReportPrivateNote(report.reportID); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps -- do not add isLoadingPrivateNotes to dependencies |
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.
do not add isLoadingPrivateNotes to dependencies
Why though?
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.
Fixed. thanks
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.
@MonilBhavsar Reverted this change as having deps on isLoadingPrivateNotes causes infinite loop as every time isLoadingPrivateNotes changes, we fetch the API again which then again changes the isLoadingPrivateNotes which creates infinite loop.
function WithReportAndPrivateNotesOrNotFound({forwardedRef, ...props}) { | ||
const {route, report, network, session} = props; | ||
const accountID = route.params.accountID; | ||
const isLoadingPrivateNotes = report.isLoadingPrivateNotes; |
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.
Is isLoadingPrivateNotes
always set or we should default to false if it doesn't exist?
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.
@MonilBhavsar No I dont want to set it to true or false. I want to keep it undefined until useEffect fetches the private notes and post fetching it sets it true or false.
Having three values of isLoadingPrivateNotes: undefined, true and false helps in knowing following:
- If isLoadingPrivateNotes is undefined or true, and the private notes are empty, this means that we are yet to load private notes OR the private notes are still loading. So in these cases, show loading indicator.
- If isLoadingPrivateNotes is defined and false, then it means we fetched private notes, so dont shon loading indicator even if private notes are empty as that's a valid case for user to not have private notes.
That's the reason why I am using isLoadingPrivateNotes !== false instead of isLoadingPrivateNotes
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.
I feel like we can skip undefined case and set isLoadingPrivateNotes
to true if we're about to, or fetching notes and then render loading indicator
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.
done. thanks.
}, [report.reportID, network.isOffline]); | ||
|
||
const isPrivateNotesEmpty = accountID ? _.isEmpty(lodashGet(report, ['privateNotes', accountID, 'note'], '')) : _.isEmpty(report.privateNotes); | ||
const shouldShowFullScreenLoadingIndicator = isLoadingPrivateNotes !== false && isPrivateNotesEmpty; |
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.
Can be simplified I think
const shouldShowFullScreenLoadingIndicator = isLoadingPrivateNotes !== false && isPrivateNotesEmpty; | |
const shouldShowFullScreenLoadingIndicator = isLoadingPrivateNotes && isPrivateNotesEmpty; |
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.
Check above comment for details.
isLoadingPrivateNotes !== false
give me true when isLoadingPrivateNotes is undefined or when isLoadingPrivateNotes is true which is what i need.
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.
done. thanks.
} | ||
|
||
// Don't show not found view if the notes are still loading, or if the notes are non-empty. | ||
if (isLoadingPrivateNotes !== false || !isPrivateNotesEmpty) { |
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.
same here
if (isLoadingPrivateNotes !== false || !isPrivateNotesEmpty) { | |
if (isLoadingPrivateNotes || !isPrivateNotesEmpty) { |
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.
Same comment as above.
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.
done. thanks.
@MonilBhavsar Answered all comments. Can you have a look? |
@MonilBhavsar 2 of my starting commits on 13 oct are not signed causing failures. Is there a documentation I can refer to sign those? |
|
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.
Looks good to me except we're now making multiple redundant API calls - GetReportPrivateNote
. This behavior doesn't exist on main branch
Screen.Recording.2023-10-26.at.8.23.51.PM.mov
@MonilBhavsar Corrected this and verified in offline and non-offline mode for all private pages. I am using |
Using underscore lib is fine. @ArekChr any specific reason why we should not use it? |
Looks like this change to only use boolean for |
@MonilBhavsar I want different behaviours when isLoadingPrivateNotes is undefined, true or false. So i dont want to set any default value to it. |
IMO having a different variable to hold that value would be better rather than deriving that from different related variable. What do you think? |
@MonilBhavsar I have set |
Yes, that's better, thanks! Also, we have conflicts to resolve. |
Reviewed and retested. Overall, everything works for me as expected. We need to update the latest main branch and retest because are 1300 commits behind it. |
I will be OOO for the remainder of this week and return on Monday. |
@MonilBhavsar Can you approve the PR if it looks good to you? All merge conflicts are resolved and new changes were reviewed by @ArekChr |
Thanks and looking
Sorry, where was this confirmed. I missed it |
@MonilBhavsar I was referring to this comment from @ArekChr. I haven't made any new changes post this comment. I merged with main branch and retested on my end though post this comment. On another note, is it possible to run the workflows as author to verify all checks are passing? |
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.
Looks good overall and works fine
@@ -36,7 +36,7 @@ export default function ( | |||
const isReportIdInRoute = props.route.params.reportID?.length; | |||
|
|||
if (shouldRequireReportID || isReportIdInRoute) { | |||
const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData && (!Object.entries(props.report ?? {}).length || !props.report?.reportID); | |||
const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.report ?? {}).length || !props.report?.reportID); |
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.
Sorry, why this change was made?
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.
@MonilBhavsar When I took merge a week back, I found that someone made a regression when renaming the file from js to tsx.
In old withReportOrNotFound.js, we had default props setting isLoadingReportData to true but the default props were removed in new tsx file.
This change is required when default props is not provided. This changes fixes the regression caused by removing default props at all places which uses withReportOrNotFound.tsx.
What I propose is to unblock this PR with this change and then revisiting on how to bring back the default props in tsx. I think adding back default props should be out of scope of this PR. What are your thoughts on this?
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.
Thanks for clarifying. Sounds good to make a follow up PR
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.
Looks good and works fine
Thank you!
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
🚀 Deployed to staging by https://github.com/MonilBhavsar in version: 1.3.96-0 🚀
|
🚀 Deployed to production by https://github.com/puneetlath in version: 1.3.96-15 🚀
|
🚀 Deployed to production by https://github.com/puneetlath in version: 1.3.97-7 🚀
|
Details
As discussed in the issue, we are doing following:
withReportAndPrivateNotesOrNotFound
which loads report and private notes for it. It showsnot found view
if report and private notes are not found. It showsloading indicator
if the report or private notes are still loading.withReportAndPrivateNotesOrNotFound
forlist
,edit
andview
page of private notes so that we make sure that report and private notes are available. This makes sure thatnot found view
is not shown when private notes pages are accessed via deep link.not found view
andloading indicator
views from thelist
,edit
andview
pages as both of these will now be handled by HOCwithReportAndPrivateNotesOrNotFound
.Fixed Issues
$ #27704
PROPOSAL: #27704 (comment)
Tests
On web and mweb platforms:
loading indicator
,not found view
andprivate notes
are shown as expected. Here list URL is:localhost:8082/r/:reportId/notes
, view URL is:localhost:8082/r/:reportId/notes/:accountId
and edit URL is:localhost:8082/r/:reportId/notes/:accountId/edit
.Not found view
andloading indicator
doesn't show unexpected behaviour i.e.Not found view
should not appear for a valid private note,loading indicator
should not be shown indefinitely etc.On native platforms: Repeat step 1 and 2 from above.
Offline tests
Try following scenarios:
Steps:
a. Login in online mode without going to the report
b. Go offline
c. Go to notes normally or via deep link
d.
loading indicator
should appear for all list, view and edit pages of private notes.Steps:
a. Login in online mode and go to the report.
b. Go offline
c. Go to notes normally or via deep link
d.
loading indicator
should appear for all list, view and edit pages of private notes.Steps:
a. Login in online mode, go to the report and then go to the notes.
b. Go offline
c. Go to notes normally or via deep link
d. Private note should appear
QA Steps
Try following scenarios:
Steps:
a. Login, go to report, and then go to private notes in normal way.
b. Verify list, edit and view pages of private notes should show private notes after few seconds of
loading indicator
.Steps:
a. Logout from Expensify
b. Login using deep link of list page of private notes.
c. Verify private notes are shown after few seconds of
loading indicator
.d. Repeat a, b & c for view and edit pages.
a. Logout from Expensify
b. Login using deep link of list, view or edit page of private notes. Ensure that either report id is incorrect or note's accountID is incorrect.
c. Verify
Not found view
is shown after few seconds ofloading indicator view
.PR Author Checklist
### Fixed Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)myBool && <MyComponent />
.src/languages/*
files and using the translation methodWaiting for Copy
label for a copy review on the original GH to get the correct copy.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)ScrollView
component to make it scrollable when more elements are added to the page.main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.Screenshots/Videos
Android: Native
android-native.mp4
Android: mWeb Chrome
android-chrome.mp4
iOS: Native
ios-native.mp4
iOS: mWeb Safari
ios-safari.mp4
MacOS: Chrome / Safari
mac-chrome.mp4
MacOS: Desktop
mac-desktop.mp4