Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4186 +/- ##
========================================
Coverage 97.42% 97.43%
========================================
Files 893 896 +3
Lines 26127 26298 +171
Branches 9438 9495 +57
========================================
+ Hits 25455 25623 +168
- Misses 666 669 +3
Partials 6 6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
4fa101b to
efb51d9
Compare
8343dd7 to
aea04d1
Compare
2e7cb8e to
1f4c85f
Compare
221927b to
4379ee3
Compare
493d050 to
fbd48d1
Compare
9dc81fc to
6c9b625
Compare
| filterFeatures: feature => { | ||
| const halfYearAgo = new Date(); | ||
| halfYearAgo.setDate(halfYearAgo.getDate() - 180); | ||
| return feature.releaseDate >= halfYearAgo; |
There was a problem hiding this comment.
nit: this implementation is unnecessary as using filterFeatures: () => true would give the same result.
If you also want to verify that the payload is correct, it is best to use:
const filterFeatures = jest.fn().returns(() => true);
, in which case you can explicitly check that the function was called the correct amount of times and with correct payload
There was a problem hiding this comment.
the purpose of this test is to provide a usage example, so I would keep it as is
src/app-layout/__tests__/runtime-feature-notifications.test.tsx
Outdated
Show resolved
Hide resolved
| }); | ||
|
|
||
| test('should not show feature prompt if all feature are seen', async () => { | ||
| mockRetrieveFeatureNotifications.mockResolvedValue({ |
There was a problem hiding this comment.
I changed this to:
mockRetrieveFeatureNotifications.mockResolvedValue({
'feature-1': 'mockDate2025.toString()',
'feature-2': 'mockDate2024.toString()',
'feature-old': 'mockDateOld.toString()',
});
and the test still passed. Why do we need these values?
There was a problem hiding this comment.
Yes, we need this test. However the dates are not important here, because we check only features ids - we check dates only when we persist values to filter out outdated values
| {formatDate(item.releaseDate)} | ||
| </Box> | ||
| {!!item.contentCategory && ( | ||
| <Box> |
There was a problem hiding this comment.
The RuntimeContentPart already wraps content with a div or span. Why do we need the surrounding Box elements for content category and content?
There was a problem hiding this comment.
yes, we need them for right paddings
| /> | ||
|
|
||
| {!!featuresPageLink && ( | ||
| <footer className={styles['runtime-feature-notifications-footer']}> |
There was a problem hiding this comment.
Should we use drawer's footer for that?
https://cloudscape.design/components/drawer/?tabId=playground&example=with-footer
There was a problem hiding this comment.
No, I tried. The footer is sticky, which we don't need here
src/popover/body.tsx
Outdated
|
|
||
| const isDialog = showDismissButton; | ||
| const shouldTrapFocus = showDismissButton && variant !== 'annotation'; | ||
| const shouldTrapFocus = trapFocus || (showDismissButton && variant !== 'annotation'); |
There was a problem hiding this comment.
Can we introduce a distinct variant for feature notifications instead?
src/internal/persistence/index.ts
Outdated
| return Promise.resolve(false); | ||
| }; | ||
|
|
||
| // eslint-disable-next-line require-await |
There was a problem hiding this comment.
nit: should we disable these two eslint rules for the entire file instead?
|
When register API is used from inside an iframe - the initial feature prompt is not shown. The "show a standalone prompt" still works - but focus is not handled correctly and click outside does not cause a dismiss. I tested that by wrapping the new test page with the IframeWrapper util. |
| fireNonCancelableEvent(onDismiss, { method: 'click-outside' }); | ||
| } | ||
| }; | ||
| window.addEventListener('click', clickListener, true); |
There was a problem hiding this comment.
This and the nodeBelongs can be a problem when the API is called from inside an iframe. I managed to confirm it by rendering the test page inside an iframe helper.
| const checkStickyState = throttle( | ||
| useCallback(() => { | ||
| if (!drawerRef.current || !footerRef.current) { | ||
| if (!drawerRef?.current || !footerRef?.current) { |
There was a problem hiding this comment.
otherwise it throws an error
| }, [featureNotificationsData, seenFeatures]); | ||
|
|
||
| const defaultFeaturesFilter = (feature: Feature<unknown>) => { | ||
| return feature.releaseDate >= subtractDaysFromDate(new Date(), 90); |
There was a problem hiding this comment.
let's use a constant for 90 as well
| const featurePromptRef = useRef<FeaturePromptProps.Ref>(null); | ||
| const { ref: featurePromptMountRef, promise: featurePromptMountPromise } = useMountRefPromise(); | ||
| const featurePromptMergedRef = useMergeRefs(featurePromptRef, featurePromptMountRef); | ||
| const allNotificationsSeen = useMemo(() => { |
There was a problem hiding this comment.
nit: this memo is not really needed - the cost of checking the data (which is already filtered) is negligible.
| resizeHandle: i18n('ariaLabels.resizeHandle', undefined), | ||
| }, | ||
| resizable: true, | ||
| defaultSize: 320, |
There was a problem hiding this comment.
nit: let's create a constant for default drawer size, too
|
|
||
| function featureNotificationsMessageHandler(event: WidgetMessage) { | ||
| if (event.type === 'registerFeatureNotifications') { | ||
| warnOnce('feature notifications', 'Feature notifications drawer is overriding the tools panel'); |
There was a problem hiding this comment.
This renders a warning when the feature notifications feature is used correctly - why?
We should only warn if the tools feature is actually used, and in that case the feature notifications must not be rendered.
There was a problem hiding this comment.
Also, we should add a unit test for the warning.
There was a problem hiding this comment.
not needed anymore as the drawer works along with tools
| * | ||
| * @returns An object containing a ref callback and a promise that resolves with the mounted element | ||
| */ | ||
| export function useMountRefPromise<T extends HTMLElement>() { |
| } | ||
|
|
||
| if (event.type === 'showFeaturePromptIfPossible') { | ||
| if (allNotificationsSeen) { |
There was a problem hiding this comment.
nit: we can simplify the check to:
if (event.type === 'showFeaturePromptIfPossible' && !allNotificationsSeen) {
featurePromptRef.current?.show();
}
Alternatively, the event mapping can be made more explicit like:
switch (event.type) {
case 'registerFeatureNotifications':
return handleRegisterFeatureNotifications(...);
case 'showFeaturePromptIfPossible':
return showFeaturePromptIfApplicable();
case 'clearFeatureNotifications':
return setFeatureNotificationsData(null);
}
|
|
||
| // Features array is already sorted in reverse chronological order (most recent first) | ||
| // Find the first feature that hasn't been seen | ||
| for (const feature of featureNotificationsData.features) { |
There was a problem hiding this comment.
nit: this can be simplified to:
featureNotificationsData.features.find(feature => !seenFeatures[feature.id]) ?? null;
or even one line:
function getLatestUnseenFeature() {
return featureNotificationsData?.features.find(feature => !seenFeatures[feature.id]) ?? null;
}
|
|
||
| function renderLatestFeaturePrompt({ triggerRef }: RenderLatestFeaturePromptProps) { | ||
| const latestFeature = getLatestUnseenFeature(); | ||
| if (!(triggerRef?.current && featureNotificationsData && latestFeature)) { |
There was a problem hiding this comment.
this can be simplified to:
if (!triggerRef.current || !latestFeature) {
return null;
}
| }} | ||
| onDismiss={event => { | ||
| if (event.detail?.method !== 'blur') { | ||
| triggerRef?.current!.focus(); |
There was a problem hiding this comment.
This .focus() already accepts options object. Can we add support for suppressTooltip in there?
There was a problem hiding this comment.
The suppress should be controlled not only when focus is set, but also when we show the feature popover. In that case, focus goes to the feature popover while the tooltip for its trigger is suppressed
| }, {}); | ||
| const filteredSeenFeaturesMap = filterOutdatedFeatures(seenFeatures); | ||
| const allFeaturesMap = { ...featuresMap, ...filteredSeenFeaturesMap }; | ||
| const __persistFeatureNotifications: |
There was a problem hiding this comment.
nit: I would abstract the persistence related code into a dedicated function, so that the logic inside onOpenFeatureNotificationsDrawer is more clear:
const onOpenFeatureNotificationsDrawer = () => {
if (!featureNotificationsData || allNotificationsSeen) {
return;
}
const persistenceConfig = featureNotificationsData.persistenceConfig ?? DEFAULT_PERSISTENCE_CONFIG;
persistFeatureNotificationsDismissState(persistenceConfig, featureNotificationsData).then((seenFeatures) => {
setSeenFeatures(seenFeatures);
setFeatureNotificationsData(data => (data ? { ...data, badge: false } : data));
});
};
pan-kot
left a comment
There was a problem hiding this comment.
Significant:
- Feature notifications must not render if tools are present;
- Dev warning should only be shown if feature notifications and tools are used together;
- The API and feature prompt do not work correctly when used from an iframe;
- The API is exported as a set of standalone methods w/o structural grouping.
Less significant:
- Test coverage is not sufficient;
- Code structure can be improved: remove tabTrap, awsuiSuppressTooltip;
- Internal APIs for persistence are accessible via public API (with a ...{} hack);
- Persisted structure includes date snapshots that are not used;
- The dedicated test page requires an URL flag to be used;
- I18n strings API properties are not descriptive enough.
Significant, but can be done as a followup:
- No dedicated test utils for the UX features;
- I18n messages cannot be overridden.
…Props for registerFeatureNotifications
Description
Introduced a feature notifications API that allows dynamically injecting a feature notifications drawer on pages using the AppLayout component.
The new API includes 3 methods:
registerFeatureNotifications- registers a new drawershowFeaturePromptIfPossible- manually shows a feature prompt next to the drawer's trigger buttonclearFeatureNotifications- clears the feature notifications drawerAlso enhanced the existing feature prompt internal component by adding dismissal context to the onDismiss method (blur, close-button, outside-click, etc.).
Related links, issue #, if available: n/a
How has this been tested?
U tests / manually
Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.