Skip to content

Commit 6583004

Browse files
author
Stephen Cefali
authored
chore(ts): converts externalIssueForm (#16308)
1 parent 63cedf0 commit 6583004

File tree

5 files changed

+72
-19
lines changed

5 files changed

+72
-19
lines changed

src/sentry/static/sentry/app/components/group/externalIssueActions.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Props = AsyncComponent['props'] & {
2121

2222
type State = AsyncComponent['state'] & {
2323
showModal: boolean;
24-
action: string | null;
24+
action: 'create' | 'link' | null;
2525
selectedIntegration: GroupIntegration;
2626
issue: IntegrationExternalIssue | null;
2727
};
@@ -31,7 +31,7 @@ class ExternalIssueActions extends AsyncComponent<Props, State> {
3131
integration: PropTypes.object.isRequired,
3232
};
3333

34-
constructor(props, context) {
34+
constructor(props: Props, context) {
3535
super(props, context);
3636

3737
this.state = {
@@ -53,7 +53,7 @@ class ExternalIssueActions extends AsyncComponent<Props, State> {
5353
: null;
5454
}
5555

56-
deleteIssue(issueId) {
56+
deleteIssue(issueId: string) {
5757
const {group, integration} = this.props;
5858
const endpoint = `/groups/${group.id}/integrations/${
5959
integration.id
@@ -89,7 +89,7 @@ class ExternalIssueActions extends AsyncComponent<Props, State> {
8989
});
9090
};
9191

92-
handleClick = action => {
92+
handleClick = (action: 'create' | 'link') => {
9393
this.setState({action});
9494
};
9595

src/sentry/static/sentry/app/components/group/externalIssueForm.jsx renamed to src/sentry/static/sentry/app/components/group/externalIssueForm.tsx

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ import {addSuccessMessage} from 'app/actionCreators/indicator';
77
import AsyncComponent from 'app/components/asyncComponent';
88
import FieldFromConfig from 'app/views/settings/components/forms/fieldFromConfig';
99
import Form from 'app/views/settings/components/forms/form';
10+
import {FieldValue} from 'app/views/settings/components/forms/model';
1011
import SentryTypes from 'app/sentryTypes';
1112
import {t} from 'app/locale';
13+
import {
14+
Group,
15+
Integration,
16+
PlatformExternalIssue,
17+
IntegrationIssueConfig,
18+
IssueConfigField,
19+
} from 'app/types';
1220

1321
const MESSAGES_BY_ACTION = {
1422
link: t('Successfully linked issue.'),
@@ -20,7 +28,19 @@ const SUBMIT_LABEL_BY_ACTION = {
2028
create: t('Create Issue'),
2129
};
2230

23-
class ExternalIssueForm extends AsyncComponent {
31+
type Props = {
32+
group: Group;
33+
integration: Integration;
34+
action: 'create' | 'link';
35+
onSubmitSuccess: (externalIssue: PlatformExternalIssue) => void;
36+
} & AsyncComponent['props'];
37+
38+
type State = {
39+
integrationDetails: IntegrationIssueConfig;
40+
dynamicFieldValues?: {[key: string]: FieldValue};
41+
} & AsyncComponent['state'];
42+
43+
class ExternalIssueForm extends AsyncComponent<Props, State> {
2444
static propTypes = {
2545
group: SentryTypes.Group.isRequired,
2646
integration: PropTypes.object.isRequired,
@@ -30,7 +50,7 @@ class ExternalIssueForm extends AsyncComponent {
3050

3151
shouldRenderBadRequests = true;
3252

33-
getEndpoints() {
53+
getEndpoints(): [string, string][] {
3454
const {group, integration, action} = this.props;
3555
return [
3656
[
@@ -40,7 +60,7 @@ class ExternalIssueForm extends AsyncComponent {
4060
];
4161
}
4262

43-
onSubmitSuccess = data => {
63+
onSubmitSuccess = (data: PlatformExternalIssue) => {
4464
addSuccessMessage(MESSAGES_BY_ACTION[this.props.action]);
4565
this.props.onSubmitSuccess(data);
4666
};
@@ -71,17 +91,19 @@ class ExternalIssueForm extends AsyncComponent {
7191
});
7292
};
7393

74-
getDynamicFields(integrationDetails) {
94+
getDynamicFields(integrationDetails?: IntegrationIssueConfig) {
7595
integrationDetails = integrationDetails || this.state.integrationDetails;
7696
const {action} = this.props;
77-
const config = integrationDetails[`${action}IssueConfig`];
97+
const config: IssueConfigField[] = integrationDetails[`${action}IssueConfig`];
7898

79-
return config
80-
.filter(field => field.updatesForm)
81-
.reduce((a, field) => ({...a, [field.name]: field.default}), {});
99+
return Object.fromEntries(
100+
config
101+
.filter((field: IssueConfigField) => field.updatesForm)
102+
.map((field: IssueConfigField) => [field.name, field.default])
103+
);
82104
}
83105

84-
onFieldChange = (label, value) => {
106+
onFieldChange = (label: string, value: FieldValue) => {
85107
const dynamicFields = this.getDynamicFields();
86108
if (label in dynamicFields) {
87109
const dynamicFieldValues = this.state.dynamicFieldValues || {};
@@ -99,7 +121,7 @@ class ExternalIssueForm extends AsyncComponent {
99121
}
100122
};
101123

102-
getOptions = (field, input) => {
124+
getOptions = (field: IssueConfigField, input: string) => {
103125
return new Promise((resolve, reject) => {
104126
if (!input) {
105127
const options = (field.choices || []).map(([value, label]) => ({value, label}));
@@ -116,14 +138,18 @@ class ExternalIssueForm extends AsyncComponent {
116138
};
117139

118140
debouncedOptionLoad = debounce(
119-
async (field, input, cb) => {
141+
async (
142+
field: IssueConfigField,
143+
input: string,
144+
cb: (err: Error | null, result?) => void
145+
) => {
120146
const query = queryString.stringify({
121147
...this.state.dynamicFieldValues,
122148
field: field.name,
123149
query: input,
124150
});
125151

126-
const url = field.url;
152+
const url = field.url || '';
127153
const separator = url.includes('?') ? '&' : '?';
128154
// We can't use the API client here since the URL is not scoped under the
129155
// API endpoints (which the client prefixes)
@@ -138,10 +164,10 @@ class ExternalIssueForm extends AsyncComponent {
138164
{trailing: true}
139165
);
140166

141-
getFieldProps = field =>
167+
getFieldProps = (field: IssueConfigField) =>
142168
field.url
143169
? {
144-
loadOptions: input => this.getOptions(field, input),
170+
loadOptions: (input: string) => this.getOptions(field, input),
145171
async: true,
146172
cache: false,
147173
onSelectResetsInput: false,
@@ -154,7 +180,7 @@ class ExternalIssueForm extends AsyncComponent {
154180
renderBody() {
155181
const {integrationDetails} = this.state;
156182
const {action, group, integration} = this.props;
157-
const config = integrationDetails[`${action}IssueConfig`];
183+
const config: IssueConfigField[] = integrationDetails[`${action}IssueConfig`];
158184

159185
const initialData = {};
160186
config.forEach(field => {

src/sentry/static/sentry/app/types/index.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,3 +701,26 @@ export type SelectValue<T> = {
701701
label: string;
702702
value: T;
703703
};
704+
705+
/**
706+
* The issue config form fields we get are basically the form fields we use in the UI but with some extra information.
707+
* Some fields marked optional in the form field are guaranteed to exist so we can mark them as required here
708+
*/
709+
710+
export type IssueConfigField = Field & {
711+
name: string;
712+
default?: string;
713+
choices?: [number | string, number | string][];
714+
url?: string;
715+
multiple?: boolean;
716+
};
717+
718+
export type IntegrationIssueConfig = {
719+
status: ObjectStatus;
720+
name: string;
721+
domainName: string;
722+
linkIssueConfig?: IssueConfigField[];
723+
createIssueConfig?: IssueConfigField[];
724+
provider: IntegrationProvider;
725+
icon: string[];
726+
};

src/sentry/static/sentry/app/views/settings/components/forms/fieldFromConfig.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ type Props = {
2424
field: Field;
2525
highlighted?: boolean;
2626
disabled?: boolean;
27+
flexibleControlStateSize?: boolean;
28+
stacked?: boolean;
29+
inline?: boolean;
2730

2831
access?: Scope[];
2932
};

src/sentry/static/sentry/app/views/settings/components/forms/type.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type BaseField = {
3434
disabled?: boolean | ((props: any) => boolean);
3535
disabledReason?: string;
3636
defaultValue?: FieldValue;
37+
updatesForm?: boolean;
3738

3839
/**
3940
* Function to format the value displayed in the undo toast. May also be

0 commit comments

Comments
 (0)