Skip to content

Commit 8fa21d6

Browse files
authored
ref: migrate teamSettings to new form system (#108525)
1 parent 5205f1b commit 8fa21d6

File tree

5 files changed

+127
-89
lines changed

5 files changed

+127
-89
lines changed

static/app/components/core/form/generatedFieldRegistry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,18 @@ export const FORM_FIELD_REGISTRY: Record<string, FormFieldDefinition> = {
238238
'Field names which data scrubbers should ignore. Separate multiple entries with a newline.'
239239
),
240240
},
241+
'team-settings.slug': {
242+
name: 'slug',
243+
formId: 'team-settings',
244+
route: '/settings/:orgId/teams/:teamId/settings/',
245+
label: t('Team Slug'),
246+
hintText: t('A unique ID used to identify the team'),
247+
},
248+
'team-settings.teamId': {
249+
name: 'teamId',
250+
formId: 'team-settings',
251+
route: '/settings/:orgId/teams/:teamId/settings/',
252+
label: t('Team ID'),
253+
hintText: t('The unique identifier for this team. It cannot be modified.'),
254+
},
241255
};

static/app/components/core/form/scrapsForm.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ function SubmitButton(props: ButtonProps) {
7070
{...props}
7171
priority="primary"
7272
type="submit"
73+
form={form.formId}
7374
disabled={isDisabled || props.disabled}
7475
/>
7576
)}

static/app/data/forms/teamSettingsFields.tsx

Lines changed: 0 additions & 30 deletions
This file was deleted.

static/app/views/settings/organizationTeams/teamSettings/index.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ describe('TeamSettings', () => {
142142
"This team is managed through your organization's identity provider. These settings cannot be modified."
143143
)
144144
).toBeInTheDocument();
145-
expect(screen.getByRole('textbox', {name: 'Team Slug'})).toBeDisabled();
145+
expect(screen.getByRole('textbox', {name: 'Team Slug'})).toHaveAttribute(
146+
'aria-disabled',
147+
'true'
148+
);
146149
expect(screen.getByTestId('button-remove-team')).toBeDisabled();
147150
});
148151
});
Lines changed: 108 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,76 @@
1-
import {Fragment, useMemo} from 'react';
2-
import cloneDeep from 'lodash/cloneDeep';
1+
import {z} from 'zod';
32

43
import {Alert} from '@sentry/scraps/alert';
54
import {Button} from '@sentry/scraps/button';
5+
import {
6+
defaultFormOptions,
7+
FieldGroup,
8+
FormSearch,
9+
useScrapsForm,
10+
} from '@sentry/scraps/form';
11+
import {Flex} from '@sentry/scraps/layout';
612

713
import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
814
import {removeTeam, updateTeamSuccess} from 'sentry/actionCreators/teams';
915
import {hasEveryAccess} from 'sentry/components/acl/access';
1016
import Confirm from 'sentry/components/confirm';
11-
import FieldGroup from 'sentry/components/forms/fieldGroup';
12-
import type {FormProps} from 'sentry/components/forms/form';
13-
import Form from 'sentry/components/forms/form';
14-
import JsonForm from 'sentry/components/forms/jsonForm';
15-
import type {FieldObject} from 'sentry/components/forms/types';
17+
import LegacyFieldGroup from 'sentry/components/forms/fieldGroup';
1618
import Panel from 'sentry/components/panels/panel';
1719
import PanelHeader from 'sentry/components/panels/panelHeader';
1820
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
19-
import teamSettingsFields from 'sentry/data/forms/teamSettingsFields';
2021
import {IconDelete} from 'sentry/icons';
2122
import {t, tct} from 'sentry/locale';
2223
import type {Team} from 'sentry/types/organization';
24+
import getApiUrl from 'sentry/utils/api/getApiUrl';
25+
import {fetchMutation, useMutation} from 'sentry/utils/queryClient';
26+
import slugify from 'sentry/utils/slugify';
2327
import useApi from 'sentry/utils/useApi';
2428
import {useNavigate} from 'sentry/utils/useNavigate';
2529
import useOrganization from 'sentry/utils/useOrganization';
2630
import {useTeamDetailsOutlet} from 'sentry/views/settings/organizationTeams/teamDetails';
2731
import {ProjectPermissionAlert} from 'sentry/views/settings/project/projectPermissionAlert';
2832

33+
const teamSettingsSchema = z.object({
34+
slug: z.string().min(1, t('Team slug is required')),
35+
teamId: z.string(),
36+
});
37+
2938
export default function TeamSettings() {
3039
const navigate = useNavigate();
3140
const organization = useOrganization();
3241
const api = useApi();
3342
const {team} = useTeamDetailsOutlet();
3443

35-
const handleSubmitSuccess: FormProps['onSubmitSuccess'] = (resp: Team, _model, id) => {
36-
// Use the old slug when triggering the update so we correctly replace the
37-
// previous team in the store
38-
updateTeamSuccess(team.slug, resp);
39-
if (id === 'slug') {
44+
const mutation = useMutation<Team, Error, {slug: string}>({
45+
mutationFn: (data: {slug: string}) =>
46+
fetchMutation<Team>({
47+
method: 'PUT',
48+
url: getApiUrl('/teams/$organizationIdOrSlug/$teamIdOrSlug/', {
49+
path: {
50+
organizationIdOrSlug: organization.slug,
51+
teamIdOrSlug: team.slug,
52+
},
53+
}),
54+
data,
55+
}),
56+
onSuccess: (resp: Team) => {
57+
updateTeamSuccess(team.slug, resp);
4058
addSuccessMessage(t('Team name changed'));
4159
navigate(`/settings/${organization.slug}/teams/${resp.slug}/settings/`, {
4260
replace: true,
4361
});
44-
}
45-
};
62+
},
63+
onError: () => {
64+
addErrorMessage(t('Unable to save change'));
65+
},
66+
});
67+
68+
const form = useScrapsForm({
69+
...defaultFormOptions,
70+
defaultValues: {slug: team.slug, teamId: team.id},
71+
validators: {onDynamic: teamSettingsSchema},
72+
onSubmit: ({value}) => mutation.mutateAsync({slug: value.slug}).catch(() => {}),
73+
});
4674

4775
const handleRemoveTeam = async () => {
4876
try {
@@ -56,28 +84,10 @@ export default function TeamSettings() {
5684
const hasTeamWrite = hasEveryAccess(['team:write'], {organization, team});
5785
const hasTeamAdmin = hasEveryAccess(['team:admin'], {organization, team});
5886
const isIdpProvisioned = team.flags['idp:provisioned'];
59-
60-
const forms = useMemo(() => {
61-
const formsConfig = cloneDeep(teamSettingsFields);
62-
63-
const teamIdField: FieldObject = {
64-
name: 'teamId',
65-
type: 'string',
66-
disabled: true,
67-
label: t('Team ID'),
68-
setValue(_, _name) {
69-
return team.id;
70-
},
71-
help: `The unique identifier for this team. It cannot be modified.`,
72-
};
73-
74-
formsConfig[0]!.fields = [...formsConfig[0]!.fields, teamIdField];
75-
76-
return formsConfig;
77-
}, [team]);
87+
const isDisabled = isIdpProvisioned || !hasTeamWrite;
7888

7989
return (
80-
<Fragment>
90+
<FormSearch route="/settings/:orgId/teams/:teamId/settings/">
8191
<SentryDocumentTitle title={t('Team Settings')} orgSlug={organization.slug} />
8292

8393
<ProjectPermissionAlert access={['team:write']} team={team} />
@@ -91,30 +101,70 @@ export default function TeamSettings() {
91101
</Alert.Container>
92102
)}
93103

94-
<Form
95-
apiMethod="PUT"
96-
apiEndpoint={`/teams/${organization.slug}/${team.slug}/`}
97-
saveOnBlur
98-
allowUndo
99-
onSubmitSuccess={handleSubmitSuccess}
100-
onSubmitError={() => addErrorMessage(t('Unable to save change'))}
101-
initialData={{
102-
name: team.name,
103-
slug: team.slug,
104-
}}
105-
>
106-
<JsonForm
107-
additionalFieldProps={{
108-
hasTeamWrite,
109-
}}
110-
disabled={isIdpProvisioned}
111-
forms={forms}
112-
/>
113-
</Form>
104+
<form.AppForm>
105+
<form.FormWrapper>
106+
<FieldGroup title={t('Team Settings')}>
107+
<form.AppField name="slug">
108+
{field => (
109+
<field.Layout.Row
110+
label={t('Team Slug')}
111+
hintText={t('A unique ID used to identify the team')}
112+
required
113+
>
114+
<field.Input
115+
value={field.state.value}
116+
onChange={value => field.handleChange(slugify(value))}
117+
placeholder="e.g. operations, web-frontend, mobile-ios"
118+
disabled={isDisabled}
119+
/>
120+
</field.Layout.Row>
121+
)}
122+
</form.AppField>
123+
<form.AppField name="teamId">
124+
{field => (
125+
<field.Layout.Row
126+
label={t('Team ID')}
127+
hintText={t(
128+
'The unique identifier for this team. It cannot be modified.'
129+
)}
130+
>
131+
<field.Input
132+
value={field.state.value}
133+
onChange={field.handleChange}
134+
disabled
135+
/>
136+
</field.Layout.Row>
137+
)}
138+
</form.AppField>
139+
140+
{isDisabled ? null : (
141+
<Flex gap="md" align="center" padding="sm">
142+
<form.Subscribe selector={state => state.values.slug !== team.slug}>
143+
{hasChanged => (
144+
<Flex
145+
flex="1"
146+
minWidth={0}
147+
style={{visibility: hasChanged ? 'visible' : 'hidden'}}
148+
>
149+
<Alert variant="info">
150+
{t('You will be redirected to the new team slug after saving.')}
151+
</Alert>
152+
</Flex>
153+
)}
154+
</form.Subscribe>
155+
<Flex gap="sm" flexShrink={0}>
156+
<Button onClick={() => form.reset()}>{t('Cancel')}</Button>
157+
<form.SubmitButton>{t('Save')}</form.SubmitButton>
158+
</Flex>
159+
</Flex>
160+
)}
161+
</FieldGroup>
162+
</form.FormWrapper>
163+
</form.AppForm>
114164

115165
<Panel>
116166
<PanelHeader>{t('Team Administration')}</PanelHeader>
117-
<FieldGroup
167+
<LegacyFieldGroup
118168
disabled={isIdpProvisioned}
119169
label={t('Remove Team')}
120170
help={t(
@@ -141,8 +191,8 @@ export default function TeamSettings() {
141191
</Button>
142192
</Confirm>
143193
</div>
144-
</FieldGroup>
194+
</LegacyFieldGroup>
145195
</Panel>
146-
</Fragment>
196+
</FormSearch>
147197
);
148198
}

0 commit comments

Comments
 (0)