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

🧹 Post-Migration Code Cleanup #1124

Merged
merged 38 commits into from
Mar 16, 2024

Conversation

JGreenlee
Copy link
Collaborator

@JGreenlee JGreenlee commented Jan 26, 2024

Summary of this PR

Code formatting

(initiative previously described in 0527569)

To be more consistent with the rest of the codebase, as a rule of thumb we're having top-level functions use 'function' synax while one-liners or nested functions are arrow functions.

Named functions that are at the top-level of a file, React component, or custom hook, should be declared as function foo() { ... }.
Anonymous / inline functions, and one-liner functions, will be declared as const bar = () = { ... };

These general rules should help to keep some consistency throughout the codebase.

in ES6, 'const' and 'let' are preferred over 'var', which is sort of outdated now.
In a few places 'var' was still used. For best practice we replace these.

-- Replace all uses of console.log or console.debug with the functions from logger.ts (logDebug, logWarn, displayError). This ensures that all log statements are recorded by both the Javascript console AND the Cordova Logger plugin
-- Format all these log statements concisely and in a unified way (longer statements make use of `template strings` with interpolation, and the statements are put on as few lines as possible to mimimize clutter)
-- Remove log statements that are deemed redundant or excessive (we want to be thorough but not spammy - avoid log statements that run on every single re-render)


Addressed review from #1106

Implemented an AlertManager

There's a bunch of places in the app where we need to show a SnackBar or "toast"-style message.
But to do this, we've been having to keep each SnackBar's visibility as state so it can show/hide depeding on its "visible" value.
It would be simpler to be able to do this from anywhere in the codebase, though, not just by setting local state.
So I refactored AlertBar to behave as a global stack of alert messages. AlertBar is instantiated once at the top level in App.tsx. A static class called AlertManager can be used to add messages to this stack from anywhere in the codebase.
As a result, components no longer need to have their own instance of AlertBar and they don't have to control the visibility themselves.
They just call AlertManager.addMessage when they want to show a message.

UI and accessibility changes

Before After
Before After
Before After

(initiative previously described in e-mission@0527569)
>To be more consistent with the rest of the codebase, as a rule of thumb we're having top-level functions use 'function' synax while one-liners or nested functions are arrow functions.

Named functions that are at the top-level of a file, React component, or custom hook, should be declared as `function foo() { ... }`.
Anonymous / inline functions, and one-liner functions, will be declared as `const bar = () = { ... };`

These general rules should help to keep some consistency throughout the codebase.
in ES6, 'const' and 'let' are preferred over 'var', which is sort of outdated now.
In a few places 'var' was still used. For best practice we replace these.
-- Replace all uses of console.log or console.debug with the functions from logger.ts (logDebug, logWarn, displayError). This ensures that all log statements are recorded by both the Javascript console AND the Cordova Logger plugin
-- Format all these log statements concisely and in a unified way (longer statements make use of `template strings` with interpolation, and the statements are put on as few lines as possible to mimimize clutter)
-- Remove log statements that are deemed redundant or excessive (we want to be thorough but not spammy - avoid log statements that run on every single re-render)
@JGreenlee JGreenlee changed the base branch from service_rewrite_2023 to master February 12, 2024 15:55
This was added in a relatively recent version of i18next; some online resources haven't updated to reflect it
https://www.i18next.com/overview/api#resolvedlanguage

`language` is what i18next detected or we have set it to. It could be either a locale code like 'en-US' or a plain language like 'en'.

`resolvedLanguage` is what language is actually being used for translation, after considering what is detected, what langauges we provided, and what fallbacks we specified.

In our case this should always be either 'en', 'es', or 'lo' - the 3 languages we support at this time.

So `resolvedLanguage` is the safe property to use and we should avoid `language`.
Although the 'ch' unit works here, is not officially a valid type for React Native Web
Changing to `25` to match the Label screen DateSelect
The `Icon` component was added in 5.11.0 and is referenced in CustomLabelSettingRow.
So we must use 5.11.0 at minimum
Reach Native Paper didn't have a standalone <Icon> component until recently. Now that it does, we can get rid of the component we had made and just use theirs.
If `getConfig` resolves to null (meaning there is no stored config & we are on the Welcome page), then we should not call Object.keys on it. (Object.keys expects a non-null input)

(There was causing an error in the console, but not a crash)
the textStyle of the datatable cell was not getting applied to the underlying Text element. With style applied on the Text component directly, the label is 12pt and bold
It is actually expected for appConfig to be initially null here, and we don't need to show an error message for it.
EnketoModal is not rendered until it is needed; it hasn't called useAppConfig() in advance. So until useAppConfig() resolves, appConfig is null.
Once it does resolve and appConfig is set, this useEffect runs again and initSurvey() gets called.
We avoid hardcoding colors to make dynamic theming easier
The initial state of modesShown should be 'labeled' only if the trip has a label; otherwise it should be 'detected' (since there will be no toggling)
With the initial state being 'detected', if there are multiple modes then the OverallTripDescriptives will show
The declaration of the Appbar.Header is repeated across every screen in the UI.
Let's unify all these declarations into one component called <NavBar>.

We already had a component called <NavBarButton>. It makes sense to put both in the same file since they will always be used together. So the new filename will be NavBar.tsx and it will export both <NavBar> and <NavBarButton>.
We had the NavBar height set at 46px to match the old AngularJS + Ionic UI. It was a bit cramped. The migration is done and we no longer have to worry about that, so what's the optimal height?
Well for accessibility reasons, click zones of the action items in the navbar should ideally be at least 44x44. If the NavBarButtons are going to be 44px tall, the NavBar should be a bit taller than that. 56px looks comfortable.
There's a bunch of places in the app where we need to show a SnackBar or "toast"-style message.
But to do this, we've been having to keep each SnackBar's visibility as state so it can show/hide depeding on its "visible" value.

It would be simpler to be able to do this from anywhere in the codebase, though, not just by setting local state.
So I refactored AlertBar to behave as a global stack of alert messages. AlertBar is instantiated once at the top level in App.tsx. A static class called AlertManager can be used to add messages to this stack from anywhere in the codebase.
As a result, components no longer need to have their own instance of AlertBar and they don't have to control the visibility themselves.
They just call AlertManager.addMessage when they want to show a message.
This comma didn't seem to have any adverse effects, but it doesn't belong here
Since AlertBar / AlertManager can now be used from anywhere in the codebase, they are not specific to the Profile screen and can be moved to a more general location
Apparently, Prettier actually does want a comma here because this is an argument to a function, and we have trailing commas enabled.
It just looks strange because it's following a JSX element
'remote notifications are not supported in the simulator' - we expect this error in the Simulator, but don't want to completely silence it so that people don't expect it to work.
An Alert works for this
On the other screens of the UI, we use a NavBarButton for clickable buttons with text. I realized the Log Out button is inconsistent with this, so I changed it to a NavBarButton.
I tested this with a screen reader and it is still accessible (since it uses the Button component it is grouped as and recognized as 1 element with role 'button')

Using a NavBarButton made the log out icon smaller than before, so while making this change I revised the NavBarButton props. I added an optional "iconSize" that can be used to override the default 20.
I removed "onPressAction" because the Button component already has "onPress" which we can just use.
I organized the styles for both NavBar and NavBarButton, and made them able to be extended on by props.
As it turns out, Enketo supports a more mobile-friendly forms interface than we've been using. I noticed the mobile-friendly interface whle using Kobotoolbox, and I wondered why it didn't show up the same in e-mission-phone.
I dug into Enketo source code and found that it automatically detects mobile devices by inspecting the user agent and platform info. (e.g. if it contains "iPhone", then mobile=true).

It applies "touch" class to the <html> element to flag this. This is indeed observed in e-mission-phone, but because we only inject Enketo styles into the views they open in (i.e. we scope those styles under .enketo-plugin in styles), the <html> element's "touch" class has no effect.

An easy solution is to just add the "touch" class at the root of our enketo content (for our purposes, we can assume e-mission-phone will always be on a phone / tablet a since it's a mobile app. We don't really need the 'mobile detection' that Enketo does)

The result of this is that the enketo forms have larger click areas on multiple-choice / multi-select questions, which is better for accessibility.
@JGreenlee
Copy link
Collaborator Author

JGreenlee commented Feb 20, 2024

Summarized the changes in the above description.

@JGreenlee JGreenlee marked this pull request as ready for review February 20, 2024 20:24
the log statement in getNotDeletedCandidates was changed from a console.log to a logDebug in 0a8ed76

The associated test checks the log statements; it was still watching for console.log and needed to be updated to logDebug
in 31d472e, the reader.onprogress, reader.onerror, and reader.onload functions here were changed from regular `function` declarations to arrow functions.

Usually this makes no difference in the operation of the code, but there is one exception.
Arrow functions don't get their own `this`, they just inherit it from the scope. When I swapped this out and made it an arrow function, I didn't notice it used `this`.

In this instance, `this` was the same thing as `reader`, which we already have a reference to. So it's an easy swap.
Copy link

codecov bot commented Feb 21, 2024

Codecov Report

Attention: Patch coverage is 82.25806% with 33 lines in your changes are missing coverage. Please review.

Project coverage is 77.49%. Comparing base (3fbc6b5) to head (14f72de).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1124      +/-   ##
==========================================
- Coverage   78.37%   77.49%   -0.89%     
==========================================
  Files          28       29       +1     
  Lines        1702     1693       -9     
  Branches      366      370       +4     
==========================================
- Hits         1334     1312      -22     
- Misses        368      381      +13     
Flag Coverage Δ
unit 77.49% <82.25%> (-0.89%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
www/js/config/useImperialConfig.ts 51.85% <100.00%> (ø)
www/js/diary/diaryHelper.ts 73.46% <ø> (ø)
www/js/i18nextInit.ts 84.61% <100.00%> (ø)
www/js/metrics/customMetricsHelper.ts 95.91% <ø> (ø)
www/js/onboarding/onboardingHelper.ts 43.24% <100.00%> (ø)
www/js/plugin/clientStats.ts 90.62% <100.00%> (+0.96%) ⬆️
www/js/plugin/logger.ts 93.75% <100.00%> (ø)
www/js/splash/customURL.ts 100.00% <100.00%> (ø)
www/js/splash/notifScheduler.ts 98.37% <100.00%> (-0.10%) ⬇️
www/js/splash/startprefs.ts 95.74% <100.00%> (-0.41%) ⬇️
... and 12 more

Instead of being purple (which is the default RN Paper theme color), AlertBars will now match the rest of the UI and use blue / blue undertones
@JGreenlee JGreenlee marked this pull request as draft February 21, 2024 03:23
'msgKey' is for translation keys, which 'success -> ' is not. We can just use 'text'. This is not user facing and does not necessarily need to be translated anyway
@JGreenlee JGreenlee marked this pull request as ready for review February 27, 2024 20:47
Since Enketo's themes use SCSS it isn't that hard to override the orange color. This makes it a bit more cohesive with the rest of the app.
These buttons aren't part of Enketo; they are provided by EnketoModal. That means we can customize them.
I made the "next" and "save" buttons at the bottom of Enketo surveys into RN Paper <Button> components, matching the rest of the UI and making the survey view more cohesive within the context of app.

Also, these buttons (as well as the "Return to beginning" and "Go to end" buttons) now have icons alongside the text.
@shankari
Copy link
Contributor

shankari commented Mar 4, 2024

@JGreenlee thanks so much for finishing this up! I will work on reviewing the code today. In parallel, it would be great if you could resolve the merge conflicts from simpler/higher priority changes that I merged earlier.

@JGreenlee
Copy link
Collaborator Author

Resolved merge conflicts

Copy link
Contributor

@shankari shankari left a comment

Choose a reason for hiding this comment

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

Great fixes - the code is significantly cleaner now!
I am going to merge this now so the other PRs can be rebased to include it, but do have some questions from the review.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not going to hold up the release for this, but it looks like we need to add some additional tests for this functionality. For example, there is no test for setListener or addMessage (or that is what `codecov tells me)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO


//forceSync and endForceSync SettingRows & their actions
export const ForceSyncRow = ({ getState }) => {
const { t } = useTranslation();
const { colors } = useTheme();

const [dataPendingVis, setDataPendingVis] = useState(false);
const [dataPushedVis, setDataPushedVis] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fine for now, but we may want to resurrect it when we revisit data sync from the phone to the server

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Behavior is the same anyway; this was replaced by a call to AlertManager

if (!tsRange.oldestTs) return;
if (!pipelineRange || !tsRange.oldestTs) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

future: why do you need this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's really just so Typescript knows pipelineRange isn't null and doesn't give a warning.

I don't think there's actually a situation where tsRange could be set if pipelineRange was null. But i had to add this check so the type definitions all check out with each other

window['Logger'].log(window['Logger'].LEVEL_DEBUG, message);
window['Logger']?.log(window['Logger'].LEVEL_DEBUG, message);
Copy link
Contributor

Choose a reason for hiding this comment

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

It is obviously a good thing to check this for robustness, but is this just to be on the safe side, or has this ever been null? As long as we wait for all plugins to be initialized before continuing initialization, window['Logger'] will always exist, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We do wait for deviceready before proceeding with the main initialization, but there a little bit of initialization that happens outside of that (specifically, the i18next initialization which does not depend on Cordova). I have noticed window['Logger'] being undefined during that.
This could be reworked in necessary.

It would also happen if there was a stray log statement at the root level of any file (not within a function body), since code at the root-level always gets executed immediately.

Comment on lines -148 to +146
console.log('STORAGE_PLUGIN: Called syncAllWebAndNativeValues ');
logDebug('STORAGE_PLUGIN: Called syncAllWebAndNativeValues');
Copy link
Contributor

Choose a reason for hiding this comment

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

lots of codecov errors here, is this function actually tested now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO potential regression
Need to look into this. This function storageSyncLocalAndNative doesn't appear to be called anywhere.

getUnifiedValue is working and is validated by testing. But storageSyncLocalAndNative is not.

This would suggest we are only syncing values for a particular key when it is being requested.
We aren't regularly syncing all keys on each app startup / resume, as was the pre-migration behavior

Comment on lines -7 to +10
callback: (url: string, urlComponents: UrlComponents) => void,
) => void;

export const onLaunchCustomURL: OnLaunchCustomURL = (rawUrl, handler) => {
handler: (url: string, urlComponents: UrlComponents) => void,
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

whoa! handler -> callback was a pretty bad regression. How did we miss it?

Copy link
Collaborator Author

@JGreenlee JGreenlee Mar 18, 2024

Choose a reason for hiding this comment

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

It was not a regression. just a bit confusing.

The type of the function (OnLaunchCustomURL) was declared separately from the function itself (onLaunchCustomURL).
The name of the second positional argument in the type definition was callback, while in the function body, it was handler.

In the rest of the codebase we don't type functions in this way, so to match style I moved the types of the arguments to be directly in the function. Which meant there is only one declaration and no confusing difference in argument names

Comment on lines -75 to +80
additionEntries.splice(index, 1);
// if entry was found in additionEntries, remove it
if (index > -1) {
additionEntries.splice(index, 1);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this was changed as part of f9c3c57, which refers to https://github.com/e-mission/e-mission-phone/pull/1106/files#r1473736741 which only talks about logging.

I don't see anything wrong with the fix, just that the motivation is not clearly outlined.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

While I was adding the first guard clause and the log statement, I realized a second guard clause would be safer here to handle the case where delete requests overlap and the entry being deleted no longer exists (like the button was double-tapped or something)

Comment on lines -65 to +64
if (!appConfig) return console.error('App config not loaded yet');
if (!rest.visible || !appConfig) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we want to log the error in this case? it is unlikely, but that is what will make it hard to figure out if it ever happens in the field. And we won't be able to reproduce it. So this log, if it exists, will hopefully save the day!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TODO add log back

@shankari shankari merged commit 5812eee into e-mission:master Mar 16, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

2 participants