Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13547-fixed-1774943336443.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Increase profile preferences reliability ([#13547](https://github.com/linode/manager/pull/13547))
91 changes: 39 additions & 52 deletions packages/manager/src/components/PrimaryNav/PrimaryNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ export interface PrimaryNavProps {
isCollapsed: boolean;
}

/** Indices for product-family accordions when the user has not saved `collapsedSideNavProductFamilies` yet. */
const DEFAULT_COLLAPSED_SIDE_NAV_INDICES = [1, 2, 3, 4, 5, 6, 7];

function getActiveSideNavGroupIndex(
groups: ProductFamilyLinkGroup<PrimaryLinkType[]>[],
pathname: string
) {
return groups.findIndex((group) => {
const filteredLinks = group.links.filter((link) => !link.hide);

return filteredLinks.some((link) => linkIsActive(pathname, link.to));
});
}

export const PrimaryNav = (props: PrimaryNavProps) => {
const { closeMenu, desktopMenuToggle, isCollapsed } = props;
const navItemsRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -131,20 +145,11 @@ export const PrimaryNav = (props: PrimaryNavProps) => {

const { isReserveIpEnabled } = useIsReserveIpEnabled();

const {
data: preferences,
error: preferencesError,
isLoading: preferencesLoading,
} = usePreferences();
const { data: preferences } = usePreferences();

const collapsedSideNavPreference =
preferences?.collapsedSideNavProductFamilies;

const collapsedAccordions = React.useMemo(
() => collapsedSideNavPreference ?? [1, 2, 3, 4, 5, 6, 7], // by default, we collapse all categories if no preference is set;
[collapsedSideNavPreference]
);

const { mutateAsync: updatePreferences } = useMutatePreferences();

const productFamilyLinkGroups: ProductFamilyLinkGroup<PrimaryLinkType[]>[] =
Expand Down Expand Up @@ -371,6 +376,30 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
]
);

const collapsedAccordions = React.useMemo(() => {
if (preferences === undefined) {
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES;
}
if (collapsedSideNavPreference !== undefined) {
return collapsedSideNavPreference;
}
const activeGroupIndex = getActiveSideNavGroupIndex(
productFamilyLinkGroups,
location.pathname
);
if (activeGroupIndex === -1) {
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES;
}
return DEFAULT_COLLAPSED_SIDE_NAV_INDICES.filter(
(idx) => idx !== activeGroupIndex
);
}, [
collapsedSideNavPreference,
preferences,
productFamilyLinkGroups,
location.pathname,
]);

const accordionClicked = React.useCallback(
(index: number) => {
let updatedCollapsedAccordions: number[];
Expand Down Expand Up @@ -434,48 +463,6 @@ export const PrimaryNav = (props: PrimaryNavProps) => {
};
}, [checkOverflow]);

// This effect will only run if the collapsedSideNavPreference is not set
// When a user lands on a page and does not have any preference set,
// we want to expand the accordion that contains the active link for convenience and discoverability
React.useEffect(() => {
// Wait for preferences to load or if there's an error
if (preferencesLoading || preferencesError) {
return;
}

// Wait for preferences data to be available (not just the field, but the whole object)
if (!preferences) {
return;
}

// If user has already set collapsedSideNavProductFamilies preference, don't override it
if (collapsedSideNavPreference) {
return;
}

// Find the index of the group containing the active link and expand it
const activeGroupIndex = productFamilyLinkGroups.findIndex((group) => {
const filteredLinks = group.links.filter((link) => !link.hide);

return filteredLinks.some((link) =>
linkIsActive(location.pathname, link.to)
);
});

if (activeGroupIndex !== -1) {
accordionClicked(activeGroupIndex);
}
}, [
accordionClicked,
location.pathname,
location.search,
productFamilyLinkGroups,
collapsedSideNavPreference,
preferences,
preferencesLoading,
preferencesError,
]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this one has always been questionable and a potential culprit for the faulty reset


let activeProductFamily = '';

return (
Expand Down
41 changes: 32 additions & 9 deletions packages/queries/src/profile/preferences.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { updateUserPreferences } from '@linode/api-v4';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
queryOptions,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query';

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

const isPreferencesMergeBase = (
value: ManagerPreferences | undefined,
): value is ManagerPreferences =>
value !== null &&
value !== undefined &&
typeof value === 'object' &&
!Array.isArray(value);

// Reference for this pattern: https://tkdodo.eu/blog/react-query-data-transformations#3-using-the-select-option
export const usePreferences = <TData = ManagerPreferences>(
select?: (data: ManagerPreferences | undefined) => TData,
Expand All @@ -32,12 +45,17 @@ export const useMutatePreferences = (replace = false) => {
if (replace) {
return updateUserPreferences(data);
}
const existingPreferences =
await queryClient.ensureQueryData<ManagerPreferences>(
profileQueries.preferences,
);
const preferencesQueryOptions = queryOptions(profileQueries.preferences);
const existingPreferences = await queryClient.ensureQueryData(
preferencesQueryOptions,
);

return updateUserPreferences({ ...existingPreferences, ...data });
},
onError: () =>
queryClient.invalidateQueries({
queryKey: profileQueries.preferences.queryKey,
}),
onMutate: (data) => updatePreferenceData(data, replace, queryClient),
});
};
Expand All @@ -49,9 +67,14 @@ export const updatePreferenceData = (
): void => {
queryClient.setQueryData<ManagerPreferences>(
profileQueries.preferences.queryKey,
(oldData) => ({
...(!replace ? oldData : {}),
...newData,
}),
(oldData) => {
if (replace) {
return { ...newData };
}
if (!isPreferencesMergeBase(oldData)) {
return oldData;
}
return { ...oldData, ...newData };
},
);
};
Loading