Skip to content

ref(issues): Extract modal into hook, allow local dismissal #89741

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

Merged
merged 2 commits into from
Apr 16, 2025
Merged
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
143 changes: 92 additions & 51 deletions static/app/views/issueDetails/actions/newIssueExperienceButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {t} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
import useMutateUserOptions from 'sentry/utils/useMutateUserOptions';
import useOrganization from 'sentry/utils/useOrganization';
import {useUser} from 'sentry/utils/useUser';
Expand All @@ -25,17 +26,104 @@ import {
} from 'sentry/views/issueDetails/issueDetailsTourModal';
import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';

/**
* This hook will cause the promotional modal to appear if:
* - All the steps have been registered
* - The tour has not been completed
* - The tour is not currently active
* - The streamline UI is enabled
* - The user's browser has not stored that they've seen the promo
*
* Returns a function that can be used to reset the modal.
*/
export function useIssueDetailsPromoModal() {
const organization = useOrganization();
const hasStreamlinedUI = useHasStreamlinedUI();
const {mutate: mutateAssistant} = useMutateAssistant();
const {
startTour,
endTour,
currentStepId,
isRegistered: isTourRegistered,
isCompleted: isTourCompleted,
} = useIssueDetailsTour();

const [localTourState, setLocalTourState] = useLocalStorageState(
Copy link
Member

Choose a reason for hiding this comment

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

If localstorage is actually necessary it seems like we would want to use it as a backup everywhere we currently use /assistant/ - do you think that's the case?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think maybe, though there are a bunch of other places we use the assistant that would need addressing so it's probably out of scope for now. If we have time we can look into the cause for this user's bad assistant state but I think it's a little early for a broad change (though it's worth noting)

If reports keep coming in we should look into it further, this page is just extremely high traffic so I'm being cautious.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also something we should note for building a TourStore to resolve the concurrent tour issues (related to #89733)!

ISSUE_DETAILS_TOUR_GUIDE_KEY,
{hasSeen: false}
);

const isPromoVisible =
isTourRegistered &&
!isTourCompleted &&
currentStepId === null &&
hasStreamlinedUI &&
!localTourState.hasSeen;

const handleEndTour = useCallback(() => {
setLocalTourState({hasSeen: true});
mutateAssistant({guide: ISSUE_DETAILS_TOUR_GUIDE_KEY, status: 'dismissed'});
endTour();
trackAnalytics('issue_details.tour.skipped', {organization});
}, [mutateAssistant, organization, endTour, setLocalTourState]);

useEffect(() => {
if (isPromoVisible) {
openModal(
props => (
<IssueDetailsTourModal
handleDismissTour={() => {
handleEndTour();
props.closeModal();
}}
handleStartTour={() => {
props.closeModal();
setLocalTourState({hasSeen: true});
startTour();
trackAnalytics('issue_details.tour.started', {
organization,
method: 'modal',
});
}}
/>
),
{
modalCss: IssueDetailsTourModalCss,
onClose: reason => {
if (reason) {
handleEndTour();
}
},
}
);
}
}, [
isPromoVisible,
mutateAssistant,
organization,
endTour,
startTour,
setLocalTourState,
handleEndTour,
]);

const resetModal = useCallback(() => {
setLocalTourState({hasSeen: false});
mutateAssistant({guide: ISSUE_DETAILS_TOUR_GUIDE_KEY, status: 'restart'});
}, [mutateAssistant, setLocalTourState]);

return {resetModal};
}

export function NewIssueExperienceButton() {
const organization = useOrganization();
const isSuperUser = isActiveSuperuser();
const {
endTour,
startTour,
currentStepId,
isRegistered: isTourRegistered,
isCompleted: isTourCompleted,
} = useIssueDetailsTour();
const {mutate: mutateAssistant} = useMutateAssistant();
const {resetModal} = useIssueDetailsPromoModal();

// XXX: We use a ref to track the previous state of tour completion
// since we only show the banner when the tour goes from incomplete to complete
Expand Down Expand Up @@ -75,51 +163,6 @@ export function NewIssueExperienceButton() {
});
}, [mutateUserOptions, organization, hasStreamlinedUI, userStreamlinePreference]);

// The promotional modal should only appear if:
// - All the steps have been registered
// - The tour has not been completed
// - The tour is not currently active
// - The streamline UI is enabled
const isPromoVisible =
isTourRegistered && !isTourCompleted && currentStepId === null && hasStreamlinedUI;

useEffect(() => {
if (isPromoVisible) {
openModal(
props => (
<IssueDetailsTourModal
handleDismissTour={() => {
mutateAssistant({guide: ISSUE_DETAILS_TOUR_GUIDE_KEY, status: 'dismissed'});
endTour();
trackAnalytics('issue_details.tour.skipped', {organization});
props.closeModal();
}}
handleStartTour={() => {
props.closeModal();
startTour();
trackAnalytics('issue_details.tour.started', {
organization,
method: 'modal',
});
}}
/>
),
{
modalCss: IssueDetailsTourModalCss,
onClose: reason => {
if (reason) {
mutateAssistant({
guide: ISSUE_DETAILS_TOUR_GUIDE_KEY,
status: 'dismissed',
});
endTour();
}
},
}
);
}
}, [isPromoVisible, mutateAssistant, organization, endTour, startTour]);

if (!hasStreamlinedUI) {
return (
<TryNewButton
Expand Down Expand Up @@ -173,9 +216,7 @@ export function NewIssueExperienceButton() {
key: 'reset-tour-modal',
label: t('Reset tour modal (Superuser only)'),
hidden: !isSuperUser || !isTourCompleted,
onAction: () => {
mutateAssistant({guide: ISSUE_DETAILS_TOUR_GUIDE_KEY, status: 'restart'});
},
onAction: resetModal,
},
];

Expand Down
Loading