1- import { Fragment , useMemo } from 'react' ;
2- import cloneDeep from 'lodash/cloneDeep' ;
1+ import { z } from 'zod' ;
32
43import { Alert } from '@sentry/scraps/alert' ;
54import { 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
713import { addErrorMessage , addSuccessMessage } from 'sentry/actionCreators/indicator' ;
814import { removeTeam , updateTeamSuccess } from 'sentry/actionCreators/teams' ;
915import { hasEveryAccess } from 'sentry/components/acl/access' ;
1016import 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' ;
1618import Panel from 'sentry/components/panels/panel' ;
1719import PanelHeader from 'sentry/components/panels/panelHeader' ;
1820import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle' ;
19- import teamSettingsFields from 'sentry/data/forms/teamSettingsFields' ;
2021import { IconDelete } from 'sentry/icons' ;
2122import { t , tct } from 'sentry/locale' ;
2223import 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' ;
2327import useApi from 'sentry/utils/useApi' ;
2428import { useNavigate } from 'sentry/utils/useNavigate' ;
2529import useOrganization from 'sentry/utils/useOrganization' ;
2630import { useTeamDetailsOutlet } from 'sentry/views/settings/organizationTeams/teamDetails' ;
2731import { 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+
2938export 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