-
Notifications
You must be signed in to change notification settings - Fork 295
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
Refactor ModuleRecoveryAlert to new notification system. #10398
base: develop
Are you sure you want to change the base?
Refactor ModuleRecoveryAlert to new notification system. #10398
Conversation
Build files for 6ee8815 are ready:
|
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 @benbowler, this is off to a good start and seems to be working well too! I've left a few points to address throughout but let's try to simplify the component a bit more in the process here. LMK if you have any questions 👍
assets/js/components/dashboard-sharing/ModuleRecoveryAlert/index.js
Outdated
Show resolved
Hide resolved
assets/js/components/dashboard-sharing/ModuleRecoveryAlert/index.js
Outdated
Show resolved
Hide resolved
'module-recovery-alert': { | ||
Component: ModuleRecoveryAlert, | ||
priority: 320, |
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 "alerts" such as this should be a higher priority (closer to 0) than informational notifications. Currently the zero data notification shows before this, which is a lower priority. The order should be preserved as it is today as in BannerNotifications
(2nd only to AdSenseAlerts
).
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've updated it relative to the other notification of this type, I could take it closer to 0 if needed. I also left a note on the AdSenseAlerts
to ensure this becomes higher priority and other refactors in this notification area are lower priority.
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, I think something like 145 would be more appropriate (less important than setup errors, but more important than scope alerts).
assets/js/components/dashboard-sharing/ModuleRecoveryAlert/index.stories.js
Show resolved
Hide resolved
|
||
export default function ModuleRecoveryAlert() { | ||
export default function ModuleRecoveryAlert( { id, Notification } ) { |
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.
Most components are simplified during the transition to the new API, however this one has grown substantially, at least in terms of total lines. Perhaps this is mostly due to my previous comment about using Description
components instead of defining inline here, but even beyond that, it seems like there is room for some additional refactoring to simplify things further.
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've much reduced the size, although it is larger as the props which were once on the BannerNotification component are now duplicated on CTA/Dismiss components.
I've also added additional functionality to disable the Recover button when no modules are selected.
assets/js/components/dashboard-sharing/ModuleRecoveryAlert/index.js
Outdated
Show resolved
Hide resolved
recoverableModules === undefined || | ||
recoverableModulesList.length === 0 |
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'm not sure it's possible for the resolved recoverableModules
to be anything other than an array, but also getRecoverableModules
returns an array of slugs, not an object (like getModules
).
recoverableModules === undefined || | |
recoverableModulesList.length === 0 | |
! recoverableModules.length |
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 part appears to be unchanged, did you mean to push something?
isSaving={ recoveringModules } | ||
/> | ||
<CTALinkSubtle | ||
id={ id } |
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.
Any reason why we aren't using the standard <Dismiss>
component in all of these cases? Then we don't need to pass onCTAClick
to dismissNotification
.
Actually, we probably can use <ActionsCTALinkDismiss>
as they provide a combination of CTALink
and Dismiss
.
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've refactored to use this component. One thing to note is that, because we can't vary the dismissExpires
independently between the CTA and Dismiss CTA the the notification won't show again if the error recurs within one day.
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 think using the <ActionsCTALink>
component might fix these gap changes between the primary and secondary CTAs in all of these VRTs.
…efactor-module-recovery-alert.
…ismiss logic and disable Recover button when modules are uncheked.
description={ description } | ||
actions={ actions } | ||
/> | ||
</Notification> |
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've refactored the component significantly, it's longer in terms of lines because props in components such as ActionsCTALinkDismiss
and Dismiss
those that were passed to the BannerNotification
component. The implementation here is clear and now reuses key shared notification components.
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 @benbowler, I spent a bit looking into what a more substantial refactor might look like for this component as it's quite complex but I think it's probably best saved for its own issue.
I'd love to see a few components and/or hooks extracted here for the various description
and actions
cases above. We should avoid having large nested if statements as it's quite hard to read and understand. Similarly, the learn more link is the same in all cases where it's present.
Feel free to address any low-hanging fruit you see if time allows, but otherwise, let's open a follow up issue to (probably rewrite) this component.
@@ -108,7 +107,6 @@ export default function BannerNotifications() { | |||
return ( | |||
<Fragment> | |||
{ adSenseModuleActive && <AdSenseAlerts /> } | |||
<ModuleRecoveryAlert /> |
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.
@jimmymadon shouldn't we be migrating these from the bottom up?
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 @benbowler – I think this is close to ready although I noticed that this is diverging a bit from our approach to the refactoring in terms of the order of components that we're touching. I'll let @jimmymadon confirm, but this may need to be parked for a bit while we address the others between this one and the <Notifications>
below it.
dismissLabel={ __( 'Remind me later', 'google-site-kit' ) } | ||
dismissOnCTAClick={ shouldDismissOnCTAClick } | ||
dismissExpires={ DAY_IN_SECONDS } | ||
ctaDismissOptions={ { skipHidingFromQueue: false } } |
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.
skipHidingFromQueue: false
shouldn't be needed anywhere, is it?
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.
We need this currently because of CTALink
which sets the default value to true
:
dismissOptions = { skipHidingFromQueue: true }, |
.. and ActionsCTALinkDismiss
uses CTALink
. We can remove this default value and update any uses of this component that require skip hiding to pass true explicitly. I think this should be a follow up issue though as it requires and audit of uses of the component and testing to confirm each of these notifications behaviour is retained.
description={ description } | ||
actions={ actions } | ||
/> | ||
</Notification> |
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 @benbowler, I spent a bit looking into what a more substantial refactor might look like for this component as it's quite complex but I think it's probably best saved for its own issue.
I'd love to see a few components and/or hooks extracted here for the various description
and actions
cases above. We should avoid having large nested if statements as it's quite hard to read and understand. Similarly, the learn more link is the same in all cases where it's present.
Feel free to address any low-hanging fruit you see if time allows, but otherwise, let's open a follow up issue to (probably rewrite) this component.
@aaemnnosttv I've made a significant refactor and simplification of this component to remove nested if/else and then took it further to split out the actions and description into their own components, let me know what you think. One effect of removing the loading bar is if you have user recoverable modules the copy and checkboxes change once the module access resolvers resolve: Screen.Recording.2025-03-12.at.10.59.20.movDo you think this is appropriate or should we await resolution of the module access checks before rendering the entire component or parts of it? Or add back a loading bar, what do you think? |
…from SimpleNotification.
Size Change: +419 B (+0.02%) Total Size: 2.07 MB
ℹ️ View Unchanged
|
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 @benbowler – this is is on the right track. I don't want to burn time here but I've left a few more points to address. LMK if you have any questions 👍
const hasMultipleRecoverableModules = useMemo( | ||
() => Object.keys( recoverableModules || {} ).length > 1, | ||
[ recoverableModules ] | ||
); | ||
const hasUserRecoverableModules = useMemo( | ||
() => !! Object.keys( userAccessibleModules || {} ).length, | ||
[ userAccessibleModules ] |
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.
- +1 to "user recoverable modules" as this is what I would have used as well. Please also update
userAccessibleModules
above as well since name is more accurate and applies there as well. - I'm not sure
useMemo
is providing a benefit here when used with[ userAccessibleModules ]
as the dependencies since it returns a new array every time (so it's likely never returning the memoized value). I think it should be okay to skip using this hook here anyways since the computation is probably less expensive than the overhead of the hook.
<Description | ||
id={ id } | ||
recoverableModules={ recoverableModules } | ||
userAccessibleModules={ userAccessibleModules } |
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.
Please also use the same naming here as well
userAccessibleModules={ userAccessibleModules } | |
userRecoverableModules={ userRecoverableModules } |
<Actions | ||
id={ id } | ||
recoverableModules={ recoverableModules } | ||
userAccessibleModules={ userAccessibleModules } |
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.
Here too
recoverableModules === undefined || | ||
recoverableModulesList.length === 0 |
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 part appears to be unchanged, did you mean to push something?
recoverableModules[ | ||
Object.keys( recoverableModules || {} )[ 0 ] | ||
]?.name |
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.
If there's only one, we can grab the first without using the key.
recoverableModules[ | |
Object.keys( recoverableModules || {} )[ 0 ] | |
]?.name | |
Object.values( recoverableModules )[ 0 ].name |
// Disable the CTA if no modules are selected to be restored. | ||
const disableCTA = useMemo( | ||
() => | ||
checkboxes !== null && | ||
! Object.values( checkboxes ).some( ( checked ) => checked ), | ||
[ checkboxes ] | ||
); | ||
|
||
// Only allow the alert to be dismissed if all recoverable modules are selected. | ||
const shouldDismissOnCTAClick = useMemo( | ||
() => | ||
Object.keys( userAccessibleModules || {} ).length === | ||
Object.values( checkboxes || {} ).filter( ( checked ) => checked ) | ||
.length, | ||
[ checkboxes, userAccessibleModules ] | ||
); |
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.
Again, useMemo
seems unnecessary for booleans that don't appear expensive to compute.
{ Object.keys( recoverableModules ).map( | ||
( slug ) => ( | ||
<li className="mdc-list-item" key={ slug }> | ||
<span className="mdc-list-item__text"> | ||
{ recoverableModules[ slug ].name } |
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.
Here we're iterating over keys only but need the value too. How about using Object.entries
?
{ hasUserRecoverableModules && ( | ||
<Fragment> | ||
{ hasMultipleRecoverableModules && ( | ||
<Fragment> | ||
{ checkboxes !== null && | ||
userAccessibleModules.map( ( slug ) => ( | ||
<div key={ slug }> | ||
<Checkbox | ||
checked={ checkboxes[ slug ] } | ||
name="module-recovery-alert-checkbox" | ||
id={ `module-recovery-alert-checkbox-${ slug }` } | ||
onChange={ () => | ||
updateCheckboxes( slug ) | ||
} | ||
value={ slug } | ||
disabled={ recoveringModules } | ||
> | ||
{ recoverableModules[ slug ].name } | ||
</Checkbox> | ||
</div> | ||
) ) } | ||
<p className="googlesitekit-publisher-win__desc"> | ||
{ __( | ||
'By recovering the selected modules, you will restore access for other users by sharing access via your Google account. This does not make any changes to external services and can be managed at any time via the dashboard sharing settings.', | ||
'google-site-kit' | ||
) } | ||
</p> | ||
</Fragment> | ||
) } | ||
{ ! hasMultipleRecoverableModules && ( |
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.
What do you think about splitting out the output into separate components for has/not-has recoverable modules?
hasMultipleRecoverableModules, | ||
} ) { | ||
const [ checkboxes, setCheckboxes ] = useState( null ); | ||
const [ recoveringModules, setRecoveringModules ] = useState( false ); |
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.
How about inProgress
?
'dashboard-sharing' | ||
); | ||
} ); | ||
|
||
const userAccessibleModules = useSelect( ( select ) => { |
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 realize this is from before, but I noticed it in some of the new components. There is a mismatch of types which can easily happen looking at the names:
recoverableModules
({ [key: string]: Object }
) – subset ofgetModules
userAccessibleModules
(string[]
) – list of module slugs: subset ofrecoverableModules
that the user has access to
Both are *Modules
but different types which is confusing. For a list of slugs a name like *ModuleSlugs
would be much more clear 👍 Even once we start integrating TypeScript, we should still use the same level of care when naming things.
TL;DR If we have a use for user recoverable module objects, then we can remove the slug
mapping at the end and move that outside to a new variable.
Summary
Addresses issue:
ModuleRecoveryAlert
to use the new Notifications datastore #9299Relevant technical choices
Notes:
SimpleNotification
was scaling strangely and fixed here I believe this is correct for all future and current uses of this notification type because it does not have a support for a right hand column for a graphic.checkboxes
is being setup as this was simpler than adding inline terneries for all uses of the checkboxes list.checkRequirements
as this already awaits resolution of thegetRecoverableModules
selector.sitekit/no-storybook-scenario-label
warnings as these will be refactored in Tidy up and remove redundant use oflabel
property in VRT scenarios #9103.PR Author Checklist
Do not alter or remove anything below. The following sections will be managed by moderators only.
Code Reviewer Checklist
Merge Reviewer Checklist