Skip to content

Commit 128efd1

Browse files
fix: [UIE-10603] - Increase profile preferences reliability (#13547)
* save progress * cleanup * Added changeset: Increase profile preferences reliability * Added changeset: Prevent setData on updateUserPreferences * feedback @bnussman-akamai * cleanup
1 parent 9a9fb94 commit 128efd1

File tree

3 files changed

+76
-61
lines changed

3 files changed

+76
-61
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Fixed
3+
---
4+
5+
Increase profile preferences reliability ([#13547](https://github.com/linode/manager/pull/13547))

packages/manager/src/components/PrimaryNav/PrimaryNav.tsx

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,20 @@ export interface PrimaryNavProps {
9494
isCollapsed: boolean;
9595
}
9696

97+
/** Indices for product-family accordions when the user has not saved `collapsedSideNavProductFamilies` yet. */
98+
const DEFAULT_COLLAPSED_SIDE_NAV_INDICES = [1, 2, 3, 4, 5, 6, 7];
99+
100+
function getActiveSideNavGroupIndex(
101+
groups: ProductFamilyLinkGroup<PrimaryLinkType[]>[],
102+
pathname: string
103+
) {
104+
return groups.findIndex((group) => {
105+
const filteredLinks = group.links.filter((link) => !link.hide);
106+
107+
return filteredLinks.some((link) => linkIsActive(pathname, link.to));
108+
});
109+
}
110+
97111
export const PrimaryNav = (props: PrimaryNavProps) => {
98112
const { closeMenu, desktopMenuToggle, isCollapsed } = props;
99113
const navItemsRef = React.useRef<HTMLDivElement>(null);
@@ -131,20 +145,11 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
131145

132146
const { isReserveIpEnabled } = useIsReserveIpEnabled();
133147

134-
const {
135-
data: preferences,
136-
error: preferencesError,
137-
isLoading: preferencesLoading,
138-
} = usePreferences();
148+
const { data: preferences } = usePreferences();
139149

140150
const collapsedSideNavPreference =
141151
preferences?.collapsedSideNavProductFamilies;
142152

143-
const collapsedAccordions = React.useMemo(
144-
() => collapsedSideNavPreference ?? [1, 2, 3, 4, 5, 6, 7], // by default, we collapse all categories if no preference is set;
145-
[collapsedSideNavPreference]
146-
);
147-
148153
const { mutateAsync: updatePreferences } = useMutatePreferences();
149154

150155
const productFamilyLinkGroups: ProductFamilyLinkGroup<PrimaryLinkType[]>[] =
@@ -371,6 +376,30 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
371376
]
372377
);
373378

379+
const collapsedAccordions = React.useMemo(() => {
380+
if (preferences === undefined) {
381+
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES;
382+
}
383+
if (collapsedSideNavPreference !== undefined) {
384+
return collapsedSideNavPreference;
385+
}
386+
const activeGroupIndex = getActiveSideNavGroupIndex(
387+
productFamilyLinkGroups,
388+
location.pathname
389+
);
390+
if (activeGroupIndex === -1) {
391+
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES;
392+
}
393+
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES.filter(
394+
(idx) => idx !== activeGroupIndex
395+
);
396+
}, [
397+
collapsedSideNavPreference,
398+
preferences,
399+
productFamilyLinkGroups,
400+
location.pathname,
401+
]);
402+
374403
const accordionClicked = React.useCallback(
375404
(index: number) => {
376405
let updatedCollapsedAccordions: number[];
@@ -434,48 +463,6 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
434463
};
435464
}, [checkOverflow]);
436465

437-
// This effect will only run if the collapsedSideNavPreference is not set
438-
// When a user lands on a page and does not have any preference set,
439-
// we want to expand the accordion that contains the active link for convenience and discoverability
440-
React.useEffect(() => {
441-
// Wait for preferences to load or if there's an error
442-
if (preferencesLoading || preferencesError) {
443-
return;
444-
}
445-
446-
// Wait for preferences data to be available (not just the field, but the whole object)
447-
if (!preferences) {
448-
return;
449-
}
450-
451-
// If user has already set collapsedSideNavProductFamilies preference, don't override it
452-
if (collapsedSideNavPreference) {
453-
return;
454-
}
455-
456-
// Find the index of the group containing the active link and expand it
457-
const activeGroupIndex = productFamilyLinkGroups.findIndex((group) => {
458-
const filteredLinks = group.links.filter((link) => !link.hide);
459-
460-
return filteredLinks.some((link) =>
461-
linkIsActive(location.pathname, link.to)
462-
);
463-
});
464-
465-
if (activeGroupIndex !== -1) {
466-
accordionClicked(activeGroupIndex);
467-
}
468-
}, [
469-
accordionClicked,
470-
location.pathname,
471-
location.search,
472-
productFamilyLinkGroups,
473-
collapsedSideNavPreference,
474-
preferences,
475-
preferencesLoading,
476-
preferencesError,
477-
]);
478-
479466
let activeProductFamily = '';
480467

481468
return (

packages/queries/src/profile/preferences.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { updateUserPreferences } from '@linode/api-v4';
2-
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2+
import {
3+
queryOptions,
4+
useMutation,
5+
useQuery,
6+
useQueryClient,
7+
} from '@tanstack/react-query';
38

49
import { queryPresets } from '../base';
510
import { profileQueries } from './profile';
@@ -8,6 +13,14 @@ import type { APIError } from '@linode/api-v4';
813
import type { ManagerPreferences } from '@linode/utilities';
914
import type { QueryClient } from '@tanstack/react-query';
1015

16+
const isPreferencesMergeBase = (
17+
value: ManagerPreferences | undefined,
18+
): value is ManagerPreferences =>
19+
value !== null &&
20+
value !== undefined &&
21+
typeof value === 'object' &&
22+
!Array.isArray(value);
23+
1124
// Reference for this pattern: https://tkdodo.eu/blog/react-query-data-transformations#3-using-the-select-option
1225
export const usePreferences = <TData = ManagerPreferences>(
1326
select?: (data: ManagerPreferences | undefined) => TData,
@@ -32,12 +45,17 @@ export const useMutatePreferences = (replace = false) => {
3245
if (replace) {
3346
return updateUserPreferences(data);
3447
}
35-
const existingPreferences =
36-
await queryClient.ensureQueryData<ManagerPreferences>(
37-
profileQueries.preferences,
38-
);
48+
const preferencesQueryOptions = queryOptions(profileQueries.preferences);
49+
const existingPreferences = await queryClient.ensureQueryData(
50+
preferencesQueryOptions,
51+
);
52+
3953
return updateUserPreferences({ ...existingPreferences, ...data });
4054
},
55+
onError: () =>
56+
queryClient.invalidateQueries({
57+
queryKey: profileQueries.preferences.queryKey,
58+
}),
4159
onMutate: (data) => updatePreferenceData(data, replace, queryClient),
4260
});
4361
};
@@ -49,9 +67,14 @@ export const updatePreferenceData = (
4967
): void => {
5068
queryClient.setQueryData<ManagerPreferences>(
5169
profileQueries.preferences.queryKey,
52-
(oldData) => ({
53-
...(!replace ? oldData : {}),
54-
...newData,
55-
}),
70+
(oldData) => {
71+
if (replace) {
72+
return { ...newData };
73+
}
74+
if (!isPreferencesMergeBase(oldData)) {
75+
return oldData;
76+
}
77+
return { ...oldData, ...newData };
78+
},
5679
);
5780
};

0 commit comments

Comments
 (0)