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
119 changes: 76 additions & 43 deletions src/components/ComposableTutorial/ComposableTutorial.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { isEmpty } from 'lodash';
import { useLocation } from '@gatsbyjs/reach-router';
import { parse, ParsedQuery, stringify } from 'query-string';
import { navigate } from 'gatsby';
Expand Down Expand Up @@ -168,9 +169,13 @@ export const showComposable = (dependencies: Record<string, string>[], currentSe
// Internal component that consumes the context
const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => {
const { currentSelections, setCurrentSelections } = useContext(ComposableContext);
const location = useLocation();
const { hash, search } = useLocation();
const { composable_options: composableOptions, children } = nodeData;
const isNavigatingRef = useRef(false);
// flag to either preserve the hash or not when navigating
// ie. if providing default selections, preserve the hash in url
// vs. if changing selections, do not preserve the hash
const preserveHash = useRef(false);
const initialLoad = useRef(true);

const validSelections = useMemo(() => {
const res: Set<string> = new Set();
Expand Down Expand Up @@ -203,60 +208,72 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => {
return res;
}, [children]);

const externalQueryParamsString = useMemo(() => {
const queryParams = parse(location.search);
const [externalQueryParamsString, internalQueryParamsString] = useMemo(() => {
const queryParams = parse(search);
const composableOptionsKeys = composableOptions.map((option) => option.value);
const res: Record<string, string> = {};
const external: Record<string, string> = {};
const internal: Record<string, string> = {};
for (const [key, value] of Object.entries(queryParams)) {
if (!composableOptionsKeys.includes(key)) {
res[key] = value as string;
external[key] = value as string;
} else {
internal[key] = value as string;
}
}
return stringify(res);
}, [composableOptions, location.search]);
return [stringify(external), stringify(internal)];
}, [composableOptions, search]);

const navigatePreservingExternalQueryParams = useCallback(
(queryString: string, preserveScroll = false, hash = '') => {
({
queryString,
hash = '',
state = {},
}: {
queryString: string;
hash?: string;
state?: { [key: string]: string | boolean };
}) => {
// Preserve hash if we are not navigating from our own useEffect
let newHash;
if (preserveHash.current) {
newHash = hash;
}
navigate(
`${queryString.startsWith('?') ? '' : '?'}${queryString}${
queryString.length > 0 && externalQueryParamsString.length > 0 ? '&' : ''
}${externalQueryParamsString}${hash ? `#${hash}` : ''}`,
{ state: { preserveScroll } }
}${externalQueryParamsString}${newHash ? newHash : ''}`,
{ state: { ...state } }
);
},
[externalQueryParamsString]
);

// takes care of query param reading and rerouting
// takes care of query param reading and rerouting on initial load
// if query params fulfill all selections, show the selections
// otherwise, fallback to getting default values from combination of local storage and node Data
useEffect(() => {
if (!isBrowser) {
// do this only on initial load
if (!isBrowser || !initialLoad.current) {
return;
}

// Skip if this useEffect was triggered by our own navigation
if (isNavigatingRef.current) {
isNavigatingRef.current = false;
return;
}
initialLoad.current = false;

// first verify if there is a hash
// if there is a hash and it belongs to a composable option,
// set the current selections that composable option to show the content with hash id
const hash = location.hash?.slice(1);
if (hash) {
const selection = refToSelection[hash];
const hashString = hash.slice(1);
const selection = refToSelection[hashString];
if (selection) {
preserveHash.current = true;
setCurrentSelections(selection);
const queryString = new URLSearchParams(selection).toString();
isNavigatingRef.current = true;
return navigatePreservingExternalQueryParams(`?${queryString}`, false, hash);
return;
}
}

// read query params
const queryParams = parse(location.search);
const queryParams = parse(search);

const [filteredParams, removedQueryParams] = filterValidQueryParams(
queryParams,
Expand All @@ -266,37 +283,54 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => {
);
// if params fulfill selections, show the current selections
if (fulfilledSelections(filteredParams, composableOptions) && Object.keys(removedQueryParams).length === 0) {
setLocalValue(LOCAL_STORAGE_KEY, filteredParams);
setCurrentSelections(filteredParams);
return;
}

// params are missing. get default values using local storage and nodeData
const localStorage: Record<string, string> = getLocalValue(LOCAL_STORAGE_KEY) ?? {};
const [defaultParams] = filterValidQueryParams(localStorage, composableOptions, validSelections, true);
const queryString = new URLSearchParams(defaultParams).toString();
navigatePreservingExternalQueryParams(`?${queryString}`);
}, [
composableOptions,
location.pathname,
location.search,
location.hash,
refToSelection,
validSelections,
setCurrentSelections,
navigatePreservingExternalQueryParams,
]);
preserveHash.current = true;
setCurrentSelections(defaultParams);
}, [hash, refToSelection, setCurrentSelections, search, composableOptions, validSelections]);

// when updating selection state, update the url and local storage with the new selections
useEffect(() => {
// if no selections, do not update the url
if (!currentSelections || isEmpty(currentSelections)) {
return;
}

setLocalValue(LOCAL_STORAGE_KEY, currentSelections);

// if query params are the same as the current selections, do not update the url
const validQueryParts = parse(internalQueryParamsString);
const allSelectionsMatch = Object.entries(currentSelections).every(
([key, value]) => validQueryParts[key] === value
);
if (allSelectionsMatch) {
return;
}

const queryString = new URLSearchParams(currentSelections).toString();
return navigatePreservingExternalQueryParams({
queryString: `?${queryString}`,
hash,
state: { preserveScroll: true },
});
}, [currentSelections, hash, internalQueryParamsString, navigatePreservingExternalQueryParams]);

const onSelect = useCallback(
(value: string, option: string, index: number) => {
// the ones that occur less than index, take it
const newSelections = { ...currentSelections, [option]: value };
const [correctedParams] = filterValidQueryParams(newSelections, composableOptions, validSelections, true);

// do not preserve hash since we are changing the selections
preserveHash.current = false;

if (validSelections.has(joinKeyValuesAsString(correctedParams))) {
setCurrentSelections(correctedParams);
const queryString = new URLSearchParams(correctedParams).toString();
return navigatePreservingExternalQueryParams(`?${queryString}`, true);
return;
}

// need to correct preceding options
Expand All @@ -313,10 +347,9 @@ const ComposableTutorialInternal = ({ nodeData, ...rest }: ComposableProps) => {
}

const [defaultParams] = filterValidQueryParams(persistSelections, composableOptions, validSelections, true);
const queryString = new URLSearchParams(defaultParams).toString();
return navigatePreservingExternalQueryParams(`?${queryString}`);
setCurrentSelections(defaultParams);
},
[composableOptions, currentSelections, validSelections, setCurrentSelections, navigatePreservingExternalQueryParams]
[composableOptions, currentSelections, validSelections, setCurrentSelections]
);

return (
Expand Down
16 changes: 13 additions & 3 deletions tests/unit/ComposableTutorial.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ const mockedGetLocalValue = jest.spyOn(BrowserStorage, 'getLocalValue');
const mockedNavigate = jest.spyOn(Gatsby, 'navigate');
const mockedUseLocation = jest.spyOn(ReachRouter, 'useLocation') as jest.SpyInstance<Partial<Location>>;

jest.mock('../../src/context/chatbot-context', () => ({
useChatbotModal: () => ({
chatbotClicked: false,
setChatbotClicked: jest.fn(),
text: '',
setText: jest.fn(),
}),
ChatbotProvider: ({ children }: { children: React.ReactNode }) => children,
}));

describe('Composable Tutorial component', () => {
beforeEach(() => {
mockedGetLocalValue.mockReset();
Expand All @@ -25,7 +35,7 @@ describe('Composable Tutorial component', () => {
renderComposable();
expect(mockedNavigate).toHaveBeenCalledWith(
'?interface=driver&language=nodejs&deployment-type=atlas&operator=queryString',
{ state: { preserveScroll: false } }
{ state: { preserveScroll: true } }
);
});

Expand All @@ -41,7 +51,7 @@ describe('Composable Tutorial component', () => {
renderComposable();
expect(mockedNavigate).toHaveBeenCalledWith(
'?deployment-type=self&interface=atlas-admin-api&operator=autocomplete',
{ state: { preserveScroll: false } }
{ state: { preserveScroll: true } }
);
});

Expand Down Expand Up @@ -84,7 +94,7 @@ describe('Composable Tutorial component', () => {
// removed bad selection of language
expect(mockedNavigate).toHaveBeenCalledWith(
'?deployment-type=self&interface=atlas-admin-api&operator=autocomplete',
{ state: { preserveScroll: false } }
{ state: { preserveScroll: true } }
);
});
});
48 changes: 24 additions & 24 deletions tests/unit/__snapshots__/ComposableTutorial.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -315,23 +315,23 @@ exports[`Composable Tutorial component prioritizes query params over local stora
>
<label
class="emotion-3"
for="select-50"
id="select-49-label"
for="select-58"
id="select-57-label"
>
Interface
</label>
<button
aria-controls="select-49-menu"
aria-describedby="select-49-description"
aria-controls="select-57-menu"
aria-describedby="select-57-description"
aria-disabled="false"
aria-expanded="false"
aria-invalid="false"
aria-label="Select your Interface"
aria-labelledby="select-49-label"
aria-labelledby="select-57-label"
class="emotion-4"
data-leafygreen-ui="button"
data-testid="leafygreen-ui-select-menubutton"
id="select-50"
id="select-58"
type="button"
value="driver"
>
Expand Down Expand Up @@ -376,23 +376,23 @@ exports[`Composable Tutorial component prioritizes query params over local stora
>
<label
class="emotion-3"
for="select-70"
id="select-69-label"
for="select-78"
id="select-77-label"
>
Language
</label>
<button
aria-controls="select-69-menu"
aria-describedby="select-69-description"
aria-controls="select-77-menu"
aria-describedby="select-77-description"
aria-disabled="false"
aria-expanded="false"
aria-invalid="false"
aria-label="Select your Language"
aria-labelledby="select-69-label"
aria-labelledby="select-77-label"
class="emotion-4"
data-leafygreen-ui="button"
data-testid="leafygreen-ui-select-menubutton"
id="select-70"
id="select-78"
type="button"
value="c"
>
Expand Down Expand Up @@ -437,23 +437,23 @@ exports[`Composable Tutorial component prioritizes query params over local stora
>
<label
class="emotion-3"
for="select-52"
id="select-51-label"
for="select-60"
id="select-59-label"
>
Deployment Type
</label>
<button
aria-controls="select-51-menu"
aria-describedby="select-51-description"
aria-controls="select-59-menu"
aria-describedby="select-59-description"
aria-disabled="false"
aria-expanded="false"
aria-invalid="false"
aria-label="Select your Deployment Type"
aria-labelledby="select-51-label"
aria-labelledby="select-59-label"
class="emotion-4"
data-leafygreen-ui="button"
data-testid="leafygreen-ui-select-menubutton"
id="select-52"
id="select-60"
type="button"
value="atlas"
>
Expand Down Expand Up @@ -498,23 +498,23 @@ exports[`Composable Tutorial component prioritizes query params over local stora
>
<label
class="emotion-3"
for="select-54"
id="select-53-label"
for="select-62"
id="select-61-label"
>
Operator
</label>
<button
aria-controls="select-53-menu"
aria-describedby="select-53-description"
aria-controls="select-61-menu"
aria-describedby="select-61-description"
aria-disabled="false"
aria-expanded="false"
aria-invalid="false"
aria-label="Select your Operator"
aria-labelledby="select-53-label"
aria-labelledby="select-61-label"
class="emotion-4"
data-leafygreen-ui="button"
data-testid="leafygreen-ui-select-menubutton"
id="select-54"
id="select-62"
type="button"
value="queryString"
>
Expand Down