import React, {Fragment, useCallback, useMemo, useState} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import { BrowserClient, defaultIntegrations, defaultStackParser, makeFetchTransport, } from '@sentry/react'; import {Event} from '@sentry/types'; import cloneDeep from 'lodash/cloneDeep'; import {addSuccessMessage} from 'sentry/actionCreators/indicator'; import {ModalRenderProps} from 'sentry/actionCreators/modal'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import Textarea from 'sentry/components/forms/controls/textarea'; import Field from 'sentry/components/forms/field'; import SelectField from 'sentry/components/forms/fields/selectField'; import {Data} from 'sentry/components/forms/types'; import ExternalLink from 'sentry/components/links/externalLink'; import {t, tct} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import OrganizationStore from 'sentry/stores/organizationStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import space from 'sentry/styles/space'; import {defined} from 'sentry/utils'; import {useLocation} from 'sentry/utils/useLocation'; import useMedia from 'sentry/utils/useMedia'; import useProjects from 'sentry/utils/useProjects'; const feedbackClient = new BrowserClient({ // feedback project under Sentry organization dsn: 'https://3c5ef4e344a04a0694d187a1272e96de@o1.ingest.sentry.io/6356259', transport: makeFetchTransport, stackParser: defaultStackParser, integrations: defaultIntegrations, }); const defaultFeedbackTypes = [ t("I don't like this feature"), t('I like this feature'), t('Other reason'), ]; export type ChildrenProps = { Body: (props: { children: React.ReactNode; showSelfHostedMessage?: boolean; }) => ReturnType; Footer: (props: { onBack?: () => void; onNext?: () => void; primaryDisabledReason?: string; secondaryAction?: React.ReactNode; submitEventData?: Event; }) => ReturnType; Header: (props: {children: React.ReactNode}) => ReturnType; onFieldChange: (field: Field, value: T[Field]) => void; state: T; }; type CustomFeedbackModal = { children: (props: ChildrenProps) => React.ReactNode; featureName: string; initialData: T; }; type DefaultFeedbackModal = { featureName: string; children?: undefined; feedbackTypes?: string[]; secondaryAction?: React.ReactNode; }; export type FeedbackModalProps = | DefaultFeedbackModal | CustomFeedbackModal; export function FeedbackModal({ Header, Body, Footer, closeModal, ...props }: FeedbackModalProps & ModalRenderProps) { const {organization} = useLegacyStore(OrganizationStore); const {projects, initiallyLoaded: projectsLoaded} = useProjects(); const location = useLocation(); const theme = useTheme(); const user = ConfigStore.get('user'); const isSelfHosted = ConfigStore.get('isSelfHosted'); const [state, setState] = useState( props.children === undefined ? ({subject: undefined, additionalInfo: undefined} as unknown as T) : props.initialData ); const isScreenSmall = useMedia(`(max-width: ${theme.breakpoints.small})`); const project = useMemo(() => { if (projectsLoaded && location.query.project) { return projects.find(p => p.id === location.query.project); } return undefined; }, [projectsLoaded, projects, location.query.project]); const handleSubmit = useCallback( (submitEventData?: Event) => { const message = `${props.featureName} feedback by ${user.email}`; const commonEventProps: Event = { message, request: { url: location.pathname, }, extra: { orgFeatures: organization?.features ?? [], orgAccess: organization?.access ?? [], projectFeatures: project?.features ?? [], }, tags: { featureName: props.featureName, }, user, level: 'info', }; if (props.children === undefined) { const feedbackTypes = props.feedbackTypes ?? defaultFeedbackTypes; feedbackClient.captureEvent({ ...commonEventProps, contexts: { feedback: { additionalInfo: state.additionalInfo?.trim() ? state.additionalInfo : null, }, }, message: state.additionalInfo?.trim() ? `${message} - ${feedbackTypes[state.subject]} - ${state.additionalInfo}` : `${message} - ${feedbackTypes[state.subject]}`, }); } else { feedbackClient.captureEvent({ ...commonEventProps, ...(submitEventData ?? {}), }); } addSuccessMessage(t('Thanks for taking the time to provide us feedback!')); closeModal(); }, [ location.pathname, closeModal, organization?.features, organization?.access, project?.features, user, props, state, ] ); const ModalHeader = useCallback( ({children: headerChildren}: {children: React.ReactNode}) => { return (

{headerChildren}

); }, [Header] ); const ModalFooter = useCallback( ({ onBack, onNext, submitEventData, primaryDisabledReason, secondaryAction, }: Parameters['Footer']>[0]) => { return (
{secondaryAction && ( {secondaryAction} )} {onBack && ( )}
); }, [Footer, isScreenSmall, closeModal, handleSubmit, state, props.children] ); const ModalBody = useCallback( ({ children: bodyChildren, showSelfHostedMessage = true, }: Parameters['Body']>[0]) => { return ( {bodyChildren} {isSelfHosted && showSelfHostedMessage && ( {tct( "You agree that any feedback you submit is subject to Sentry's [privacyPolicy:Privacy Policy] and Sentry may use such feedback without restriction or obligation.", { privacyPolicy: , } )} )} ); }, [Body, isSelfHosted] ); function handleFieldChange(field: Field, value: T[Field]) { const newState = cloneDeep(state); newState[field] = value; setState(newState); } if (props.children === undefined) { const feedbackTypes = props.feedbackTypes ?? defaultFeedbackTypes; return ( {t('Submit Feedback')} ({ value: index, label: feedbackType, }))} placeholder={t('Select type of feedback')} value={state.subject} onChange={value => setState({...state, subject: value})} flexibleControlStateSize stacked required />