diff --git a/src/components/codeContext.tsx b/src/components/codeContext.tsx index 357dd8454462e..9fe687996c091 100644 --- a/src/components/codeContext.tsx +++ b/src/components/codeContext.tsx @@ -5,6 +5,8 @@ import Cookies from 'js-cookie'; import {isLocalStorageAvailable} from 'sentry-docs/utils'; +import {OnboardingOptionType} from './onboarding'; + type ProjectCodeKeywords = { API_URL: string; DSN: string; @@ -99,12 +101,14 @@ type CodeSelection = { type CodeContextType = { codeKeywords: CodeKeywords; isLoading: boolean; + onboardingOptions: OnboardingOptionType[]; sharedKeywordSelection: [ Record, React.Dispatch>, ]; storedCodeSelection: SelectedCodeTabs; updateCodeSelection: (selection: CodeSelection) => void; + updateOnboardingOptions: (options: OnboardingOptionType[]) => void; }; export const CodeContext = createContext(null); @@ -297,6 +301,7 @@ export function CodeContextProvider({children}: {children: React.ReactNode}) { const [codeKeywords, setCodeKeywords] = useState(cachedCodeKeywords ?? DEFAULTS); const [isLoading, setIsLoading] = useState(cachedCodeKeywords ? false : true); const [storedCodeSelection, setStoredCodeSelection] = useState({}); + const [onboardingOptions, setOnboardingOptions] = useState([]); // populate state using localstorage useEffect(() => { @@ -342,6 +347,8 @@ export function CodeContextProvider({children}: {children: React.ReactNode}) { updateCodeSelection, sharedKeywordSelection, isLoading, + onboardingOptions, + updateOnboardingOptions: options => setOnboardingOptions(options), }; return {children}; diff --git a/src/components/codeTabs.tsx b/src/components/codeTabs.tsx index 28f0aab73b82b..096fb829d6587 100644 --- a/src/components/codeTabs.tsx +++ b/src/components/codeTabs.tsx @@ -14,6 +14,7 @@ import styled from '@emotion/styled'; import {CodeBlockProps} from './codeBlock'; import {CodeContext} from './codeContext'; import {KEYWORDS_REGEX, ORG_AUTH_TOKEN_REGEX} from './codeKeywords'; +import {updateElementsVisibilityForOptions} from './onboarding'; import {SignInNote} from './signInNote'; // human readable versions of names @@ -99,6 +100,12 @@ export function CodeTabs({children}: CodeTabProps) { } }, [codeContext?.storedCodeSelection, groupId, possibleChoices]); + // react to possible changes in options when switching tabs + useEffect(() => { + updateElementsVisibilityForOptions(codeContext?.onboardingOptions || [], false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTabIndex]); + const buttons = possibleChoices.map((choice, idx) => ( somewhere on the page @@ -119,12 +121,65 @@ export function OnboardingOption({ ); } +/** + * Updates DOM elements' visibility based on selected onboarding options + */ +export function updateElementsVisibilityForOptions( + options: OnboardingOptionType[], + touchedOptions: boolean +) { + options.forEach(option => { + if (option.disabled) { + return; + } + const targetElements = document.querySelectorAll( + `[data-onboarding-option="${option.id}"]` + ); + + targetElements.forEach(el => { + const hiddenForThisOption = el.dataset.hideForThisOption === 'true'; + if (hiddenForThisOption) { + el.classList.toggle('hidden', option.checked); + } else { + el.classList.toggle('hidden', !option.checked); + } + // only animate things when user has interacted with the options + if (touchedOptions) { + if (el.classList.contains('code-line')) { + el.classList.toggle('animate-line', option.checked); + } + // animate content, account for inverted logic for hiding + else { + el.classList.toggle( + 'animate-content', + hiddenForThisOption ? !option.checked : option.checked + ); + } + } + }); + if (option.checked && optionDetails[option.id].deps?.length) { + const dependenciesSelector = optionDetails[option.id].deps!.map( + dep => `[data-onboarding-option="${dep}"]` + ); + const dependencies = document.querySelectorAll( + dependenciesSelector.join(', ') + ); + + dependencies.forEach(dep => { + dep.classList.remove('hidden'); + }); + } + }); +} + export function OnboardingOptionButtons({ options: initialOptions, }: { // convenience to allow passing option ids as strings when no additional config is required options: (OnboardingOptionType | OptionId)[]; }) { + const codeContext = useContext(CodeContext); + const normalizedOptions = initialOptions.map(option => { if (typeof option === 'string') { return { @@ -187,49 +242,15 @@ export function OnboardingOptionButtons({ }); }); } + + // sync local state to global useEffect(() => { - options.forEach(option => { - if (option.disabled) { - return; - } - const targetElements = document.querySelectorAll( - `[data-onboarding-option="${option.id}"]` - ); - targetElements.forEach(el => { - const hiddenForThisOption = el.dataset.hideForThisOption === 'true'; - if (hiddenForThisOption) { - el.classList.toggle('hidden', option.checked); - } else { - el.classList.toggle('hidden', !option.checked); - } - // only animate things when user has interacted with the options - if (touchedOptions) { - if (el.classList.contains('code-line')) { - el.classList.toggle('animate-line', option.checked); - } - // animate content, account for inverted logic for hiding - else { - el.classList.toggle( - 'animate-content', - hiddenForThisOption ? !option.checked : option.checked - ); - } - } - }); - if (option.checked && optionDetails[option.id].deps?.length) { - const dependenciesSelecor = optionDetails[option.id].deps!.map( - dep => `[data-onboarding-option="${dep}"]` - ); - const dependencies = document.querySelectorAll( - dependenciesSelecor.join(', ') - ); + codeContext?.updateOnboardingOptions(options); + }, [options, codeContext]); - dependencies.forEach(dep => { - dep.classList.remove('hidden'); - }); - } - }); - }, [options, touchedOptions]); + useEffect(() => { + updateElementsVisibilityForOptions(options, touchedOptions); + }, [options, touchOptions, touchedOptions]); return (