import {Component, Fragment} from 'react'; import styled from '@emotion/styled'; import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator'; import {Client} from 'sentry/api'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import TextArea from 'sentry/components/forms/controls/textarea'; import Panel from 'sentry/components/panels/panel'; import PanelBody from 'sentry/components/panels/panelBody'; import PanelHeader from 'sentry/components/panels/panelHeader'; import TimeSince from 'sentry/components/timeSince'; import {t} from 'sentry/locale'; import MemberListStore from 'sentry/stores/memberListStore'; import ProjectsStore from 'sentry/stores/projectsStore'; import type {Organization, Project, Team} from 'sentry/types'; import {defined} from 'sentry/utils'; import {trackIntegrationAnalytics} from 'sentry/utils/integrationUtil'; import RuleBuilder from './ruleBuilder'; const defaultProps = { urls: [] as string[], paths: [] as string[], disabled: false, }; type Props = { dateUpdated: string | null; initialText: string; onCancel: () => void; organization: Organization; /** * Used for analytics */ page: 'issue_details' | 'project_settings'; project: Project; onSave?: (text: string | null) => void; } & typeof defaultProps; type State = { error: null | { raw: string[]; }; hasChanges: boolean; text: string | null; }; class OwnerInput extends Component { static defaultProps = defaultProps; state: State = { hasChanges: false, text: null, error: null, }; parseError(error: State['error']) { const text = error?.raw?.[0]; if (!text) { return null; } if (text.startsWith('Invalid rule owners:')) { return {text}; } return ( ); } handleUpdateOwnership = () => { const {organization, project, onSave, page, initialText} = this.props; const {text} = this.state; this.setState({error: null}); const api = new Client(); const request = api.requestPromise( `/projects/${organization.slug}/${project.slug}/ownership/`, { method: 'PUT', data: {raw: text || ''}, } ); request .then(() => { addSuccessMessage(t('Updated issue ownership rules')); this.setState( { hasChanges: false, text, }, () => onSave && onSave(text) ); trackIntegrationAnalytics('project_ownership.saved', { page, organization, net_change: (text?.split('\n').filter(x => x).length ?? 0) - initialText.split('\n').filter(x => x).length, }); }) .catch(error => { this.setState({error: error.responseJSON}); if (error.status === 403) { addErrorMessage( t( "You don't have permission to modify issue ownership rules for this project" ) ); } else if ( error.status === 400 && error.responseJSON.raw && error.responseJSON.raw[0].startsWith('Invalid rule owners:') ) { addErrorMessage( t( 'Unable to save issue ownership rule changes: %s', error.responseJSON.raw[0] ) ); } else { addErrorMessage(t('Unable to save issue ownership rule changes')); } }); return request; }; mentionableUsers() { return MemberListStore.getAll().map(member => ({ id: member.id, display: member.email, email: member.email, })); } mentionableTeams() { const {project} = this.props; const projectWithTeams = ProjectsStore.getBySlug(project.slug); if (!projectWithTeams) { return []; } return projectWithTeams.teams.map((team: Team) => ({ id: team.id, display: `#${team.slug}`, email: team.id, })); } handleChange = (e: React.ChangeEvent) => { this.setState({ hasChanges: true, text: e.target.value, }); }; handleAddRule = (rule: string) => { const {initialText} = this.props; this.setState( ({text}) => ({ text: (text || initialText) + '\n' + rule, }), this.handleUpdateOwnership ); }; render() { const {project, organization, disabled, urls, paths, initialText, dateUpdated} = this.props; const {hasChanges, text, error} = this.state; const hasStreamlineTargetingFeature = organization.features.includes( 'streamline-targeting-context' ); return ( {!hasStreamlineTargetingFeature && ( )}
{ if (e.metaKey && e.key === 'Enter') { this.handleUpdateOwnership(); } }} > {t('Ownership Rules')} {dateUpdated && ( {t('Last Edited')} )}
{this.parseError(error)}
); } } const TEXTAREA_PADDING = 4; const TEXTAREA_LINE_HEIGHT = 24; const ActionBar = styled('div')` display: flex; align-items: center; justify-content: space-between; padding-top: 10px; `; const SyntaxOverlay = styled('div')<{line: number}>` position: absolute; top: ${({line}) => TEXTAREA_PADDING + line * TEXTAREA_LINE_HEIGHT + 1}px; width: 100%; height: ${TEXTAREA_LINE_HEIGHT}px; background-color: ${p => p.theme.error}; opacity: 0.1; pointer-events: none; `; const StyledTextArea = styled(TextArea)` min-height: 140px; overflow: auto; outline: 0; width: 100%; resize: none; margin: 1px 0 0 0; word-break: break-all; white-space: pre-wrap; padding-top: ${TEXTAREA_PADDING}px; line-height: ${TEXTAREA_LINE_HEIGHT}px; height: 450px; border-width: 0; border-top-left-radius: 0; border-top-right-radius: 0; `; const InvalidOwners = styled('div')` color: ${p => p.theme.error}; font-weight: bold; margin-top: 12px; `; const SyncDate = styled('div')` font-weight: normal; text-transform: none; `; export default OwnerInput;