Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(platform): Update onboarding options on tab switch #12913

Merged
merged 2 commits into from
Mar 7, 2025
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
7 changes: 7 additions & 0 deletions src/components/codeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,12 +101,14 @@ type CodeSelection = {
type CodeContextType = {
codeKeywords: CodeKeywords;
isLoading: boolean;
onboardingOptions: OnboardingOptionType[];
sharedKeywordSelection: [
Record<string, number>,
React.Dispatch<Record<string, number>>,
];
storedCodeSelection: SelectedCodeTabs;
updateCodeSelection: (selection: CodeSelection) => void;
updateOnboardingOptions: (options: OnboardingOptionType[]) => void;
};

export const CodeContext = createContext<CodeContextType | null>(null);
Expand Down Expand Up @@ -297,6 +301,7 @@ export function CodeContextProvider({children}: {children: React.ReactNode}) {
const [codeKeywords, setCodeKeywords] = useState(cachedCodeKeywords ?? DEFAULTS);
const [isLoading, setIsLoading] = useState<boolean>(cachedCodeKeywords ? false : true);
const [storedCodeSelection, setStoredCodeSelection] = useState<SelectedCodeTabs>({});
const [onboardingOptions, setOnboardingOptions] = useState<OnboardingOptionType[]>([]);

// populate state using localstorage
useEffect(() => {
Expand Down Expand Up @@ -342,6 +347,8 @@ export function CodeContextProvider({children}: {children: React.ReactNode}) {
updateCodeSelection,
sharedKeywordSelection,
isLoading,
onboardingOptions,
updateOnboardingOptions: options => setOnboardingOptions(options),
};

return <CodeContext.Provider value={result}>{children}</CodeContext.Provider>;
Expand Down
7 changes: 7 additions & 0 deletions src/components/codeTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) => (
<TabButton
key={idx}
Expand Down
107 changes: 64 additions & 43 deletions src/components/onboarding/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use client';

import {ReactNode, useEffect, useReducer, useState} from 'react';
import {ReactNode, useContext, useEffect, useReducer, useState} from 'react';
import {QuestionMarkCircledIcon} from '@radix-ui/react-icons';
import * as Tooltip from '@radix-ui/react-tooltip';
import {Button, Checkbox, Theme} from '@radix-ui/themes';

import styles from './styles.module.scss';

import {CodeContext} from '../codeContext';

const optionDetails: Record<
OptionId,
{
Expand Down Expand Up @@ -74,7 +76,7 @@ const OPTION_IDS = [

type OptionId = (typeof OPTION_IDS)[number];

type OnboardingOptionType = {
export type OnboardingOptionType = {
/**
* Unique identifier for the option, will control the visibility
* of `<OnboardingOption optionId="this_id"` /> somewhere on the page
Expand Down Expand Up @@ -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<HTMLDivElement>(
`[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<HTMLDivElement>(
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 {
Expand Down Expand Up @@ -187,49 +242,15 @@ export function OnboardingOptionButtons({
});
});
}

// sync local state to global
useEffect(() => {
options.forEach(option => {
if (option.disabled) {
return;
}
const targetElements = document.querySelectorAll<HTMLDivElement>(
`[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<HTMLDivElement>(
dependenciesSelecor.join(', ')
);
codeContext?.updateOnboardingOptions(options);
}, [options, codeContext]);

dependencies.forEach(dep => {
dep.classList.remove('hidden');
});
}
});
}, [options, touchedOptions]);
useEffect(() => {
updateElementsVisibilityForOptions(options, touchedOptions);
}, [options, touchOptions, touchedOptions]);

return (
<div className="onboarding-options flex flex-wrap gap-3 py-2 bg-[var(--white)] dark:bg-[var(--gray-1)] sticky top-[80px] z-[4] shadow-[var(--shadow-6)] transition">
Expand Down
Loading