From 73210243a52cf7eb2037e5128994df7c3c97d6ec Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 09:12:28 +0200 Subject: [PATCH 01/16] ref(forms): Migrate addCodeOwnerModal off legacy Form Replace the legacy Form + SelectField wrapper with useScrapsForm and field.Select. The form value drives the codeowners file query via useStore, keeping behavior identical while removing the deprecated form system. Refs LINEAR-DE-999 --- .../addCodeOwnerModal.spec.tsx | 37 +-- .../projectOwnership/addCodeOwnerModal.tsx | 252 ++++++++++-------- 2 files changed, 156 insertions(+), 133 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx index 660f46f1948cfb..cde5cc3a3c6d97 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx @@ -5,7 +5,6 @@ import {RepositoryFixture} from 'sentry-fixture/repository'; import {RepositoryProjectPathConfigFixture} from 'sentry-fixture/repositoryProjectPathConfig'; import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; -import {selectEvent} from 'sentry-test/selectEvent'; import { makeClosableHeader, @@ -84,13 +83,14 @@ describe('AddCodeOwnerModal', () => { /> ); - await waitFor(() => - selectEvent.select( - screen.getByText('--'), - `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}` - ) + await userEvent.click(await screen.findByRole('textbox')); + await userEvent.click( + await screen.findByRole('menuitemradio', { + name: `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}`, + }) ); - expect(screen.getByTestId('icon-check-mark')).toBeInTheDocument(); + + expect(await screen.findByTestId('icon-check-mark')).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Preview File'})).toHaveAttribute( 'href', @@ -117,11 +117,11 @@ describe('AddCodeOwnerModal', () => { /> ); - await waitFor(() => - selectEvent.select( - screen.getByText('--'), - `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}` - ) + await userEvent.click(await screen.findByRole('textbox')); + await userEvent.click( + await screen.findByRole('menuitemradio', { + name: `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}`, + }) ); expect(screen.getByText('No codeowner file found.')).toBeInTheDocument(); @@ -153,14 +153,15 @@ describe('AddCodeOwnerModal', () => { project={project} /> ); - await waitFor(() => - selectEvent.select( - screen.getByText('--'), - `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}` - ) + + await userEvent.click(await screen.findByRole('textbox')); + await userEvent.click( + await screen.findByRole('menuitemradio', { + name: `Repo Name: ${codeMapping.repoName}, Stack Trace Root: ${codeMapping.stackRoot}, Source Code Root: ${codeMapping.sourceRoot}`, + }) ); - await userEvent.click(screen.getByRole('button', {name: 'Add File'})); + await userEvent.click(await screen.findByRole('button', {name: 'Add File'})); await waitFor(() => { expect(addFileRequest).toHaveBeenCalledWith( diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 107edc3aa1ec42..e524ee2d34c6ab 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,21 +1,16 @@ -import {Fragment, useState, type Dispatch, type SetStateAction} from 'react'; +import {Fragment} from 'react'; import styled from '@emotion/styled'; -import { - skipToken, - useQuery, - useMutation, - type UseMutationResult, -} from '@tanstack/react-query'; +import {skipToken, useMutation, useQuery} from '@tanstack/react-query'; +import {z} from 'zod'; import {Alert} from '@sentry/scraps/alert'; import {Button, LinkButton} from '@sentry/scraps/button'; +import {defaultFormOptions, useScrapsForm, useStore} from '@sentry/scraps/form'; import {Flex} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; -import {SelectField} from 'sentry/components/forms/fields/selectField'; -import {Form} from 'sentry/components/forms/form'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {Panel} from 'sentry/components/panels/panel'; @@ -42,10 +37,14 @@ type Props = { onSave?: (data: CodeOwner) => void; } & ModalRenderProps; -type TCodeownersPayload = {codeMappingId: string | null; raw: string}; -type TCodeownersData = CodeOwner; -type TCodeownersError = RequestError; -type TCodeownersVariables = [TCodeownersPayload]; +const schema = z.object({ + codeMappingId: z + .string() + .nullable() + .refine(v => v !== null, t('Code mapping is required')), +}); + +type FormValues = z.input; export function AddCodeOwnerModal({ organization, @@ -85,36 +84,73 @@ export function AddCodeOwnerModal({ {staleTime: Infinity} ); - const [codeMappingId, setCodeMappingId] = useState(null); + if (isCodeMappingsPending || isIntegrationsPending) { + return ; + } + if (isCodeMappingsError || isIntegrationsError) { + return ; + } - const {data: codeownersFile} = useQuery( - apiOptions.as()( - '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/', - { - path: codeMappingId - ? {organizationIdOrSlug: organization.slug, configId: codeMappingId} - : skipToken, - staleTime: Infinity, - } - ) + if (!codeMappings.length) { + return ( + +
{t('Add Code Owner File')}
+ + + +
+ ); + } + + return ( + ); +} + +function ApplyCodeMappings({ + Header, + Body, + Footer, + closeModal, + codeMappings, + organization, + project, + onSave, +}: { + Body: ModalRenderProps['Body']; + Footer: ModalRenderProps['Footer']; + Header: ModalRenderProps['Header']; + closeModal: ModalRenderProps['closeModal']; + codeMappings: RepositoryProjectPathConfig[]; + onSave: ((data: CodeOwner) => void) | undefined; + organization: Organization; + project: Project; +}) { + const baseUrl = `/settings/${organization.slug}/integrations/`; - const mutation = useMutation({ - mutationFn: ([payload]: TCodeownersVariables) => { - return fetchMutation({ + const defaultValues: FormValues = {codeMappingId: null}; + + const mutation = useMutation< + CodeOwner, + RequestError, + {codeMappingId: string; raw: string} + >({ + mutationFn: payload => + fetchMutation({ method: 'POST', url: `/projects/${organization.slug}/${project.slug}/codeowners/`, options: {}, data: payload, - }); - }, - onSuccess: d => { - const codeMapping = codeMappings?.find( - mapping => mapping.id === codeMappingId?.toString() - ); - onSave?.({...d, codeMapping}); - closeModal(); - }, + }), onError: err => { if (err.responseJSON && !('raw' in err.responseJSON)) { addErrorMessage( @@ -127,39 +163,80 @@ export function AddCodeOwnerModal({ gcTime: 0, }); + const form = useScrapsForm({ + ...defaultFormOptions, + defaultValues, + validators: {onDynamic: schema}, + }); + + const codeMappingId = useStore(form.store, state => state.values.codeMappingId); + + const {data: codeownersFile} = useQuery( + apiOptions.as()( + '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/', + { + path: codeMappingId + ? {organizationIdOrSlug: organization.slug, configId: codeMappingId} + : skipToken, + staleTime: Infinity, + } + ) + ); + const addFile = () => { - if (codeownersFile) { - mutation.mutate([{codeMappingId, raw: codeownersFile.raw}]); + if (!codeownersFile || !codeMappingId) { + return; } + mutation.mutate( + {codeMappingId, raw: codeownersFile.raw}, + { + onSuccess: data => { + const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); + onSave?.({...data, codeMapping}); + closeModal(); + }, + } + ); }; - if (isCodeMappingsPending || isIntegrationsPending) { - return ; - } - if (isCodeMappingsError || isIntegrationsError) { - return ; - } - return ( - +
{t('Add Code Owner File')}
- {codeMappings.length ? ( - - ) : ( - - )} + + {field => ( + + ({ + value: cm.id, + label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, + }))} + /> + + )} + + + + {codeownersFile ? ( + + ) : ( + + )} + {mutation.isError && mutation.error.responseJSON?.raw ? ( + + ) : null} +
-
- ); -} - -function ApplyCodeMappings({ - codeMappingId, - codeMappings, - codeownersFile, - mutation, - organization, - setCodeMappingId, -}: { - codeMappingId: string | null; - codeMappings: RepositoryProjectPathConfig[]; - codeownersFile: CodeownersFile | undefined; - mutation: UseMutationResult; - organization: Organization; - setCodeMappingId: Dispatch>; -}) { - const baseUrl = `/settings/${organization.slug}/integrations/`; - return ( -
- ({ - value: cm.id, - label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, - }))} - onChange={setCodeMappingId} - required - inline={false} - flexibleControlStateSize - stacked - /> - - - {codeownersFile ? ( - - ) : ( - - )} - {mutation.isError && mutation.error.responseJSON?.raw ? ( - - ) : null} - - + ); } @@ -334,10 +360,6 @@ function ErrorMessage({ ); } -const StyledSelectField = styled(SelectField)` - border-bottom: None; - padding-right: 16px; -`; const FileResult = styled('div')` width: inherit; `; From 141dcad8fd68d3f645bd5dbf670df6cec9e7213b Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 09:33:42 +0200 Subject: [PATCH 02/16] ref(forms): Fix addCodeOwnerModal spacing Use Stack with gap to space the select and result panel, and override the Panel's default margin-bottom so spacing is driven entirely by the Stack layout primitive. --- .../projectOwnership/addCodeOwnerModal.tsx | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index e524ee2d34c6ab..7115d53f144dfa 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -6,7 +6,7 @@ import {z} from 'zod'; import {Alert} from '@sentry/scraps/alert'; import {Button, LinkButton} from '@sentry/scraps/button'; import {defaultFormOptions, useScrapsForm, useStore} from '@sentry/scraps/form'; -import {Flex} from '@sentry/scraps/layout'; +import {Flex, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; @@ -203,22 +203,22 @@ function ApplyCodeMappings({
{t('Add Code Owner File')}
- - {field => ( - - ({ - value: cm.id, - label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, - }))} - /> - - )} - + + + {field => ( + + ({ + value: cm.id, + label: `Repo Name: ${cm.repoName}, Stack Trace Root: ${cm.stackRoot}, Source Code Root: ${cm.sourceRoot}`, + }))} + /> + + )} + - {codeownersFile ? ( ) : ( @@ -232,7 +232,7 @@ function ApplyCodeMappings({ errorJSON={mutation.error.responseJSON as {raw?: string}} /> ) : null} - +
-
-
+ ); } From 9ee4f31684e1e1b862fae00906cff4fb07f8a2f3 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:04:56 +0200 Subject: [PATCH 05/16] ref(forms): Clean up a11y and translations - Drop redundant aria-label on Add File button (visible text already provides the accessible name) - Wrap the Setup Integration LinkButton text in t() - Remove duplicate text inside the tct strong tag; tct injects the template's inner text as children --- .../project/projectOwnership/addCodeOwnerModal.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 5cc400459fad16..cebe2fccb98ecf 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -290,12 +290,7 @@ function AddFileButton({ }; return ( - ); @@ -350,7 +345,7 @@ function LinkCodeOwners({organization}: {organization: Organization}) {
{t('Install a GitHub or GitLab integration to use this feature.')}
- Setup Integration + {t('Setup Integration')} @@ -420,7 +415,7 @@ function ErrorMessage({ )} {tct( '[addAndSkip:Add and Skip Missing Associations] will add your codeowner file and skip any rules that having missing associations. You can add associations later for any skipped rules.', - {addAndSkip: Add and Skip Missing Associations} + {addAndSkip: } )} From e3d2a9cb120eb302e8501f6a04ca877c28330c2e Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:15:58 +0200 Subject: [PATCH 06/16] ref(forms): Replace styled wrappers with scraps primitives - Panel + styled PanelBody grids -> Container + Grid - IntegrationsList styled div -> Stack with paddingTop - LinkButton with child icon + styled name -> icon prop - Bare div text -> Text as=p - Drops styled, Panel, PanelBody imports --- .../projectOwnership/addCodeOwnerModal.tsx | 69 ++++++------------- 1 file changed, 21 insertions(+), 48 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index cebe2fccb98ecf..f32d7d95914f5e 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,5 +1,4 @@ import {Fragment} from 'react'; -import styled from '@emotion/styled'; import { skipToken, useMutation, @@ -11,15 +10,14 @@ import {z} from 'zod'; import {Alert} from '@sentry/scraps/alert'; import {Button, LinkButton} from '@sentry/scraps/button'; import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; -import {Flex, Stack} from '@sentry/scraps/layout'; +import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; -import {Panel} from 'sentry/components/panels/panel'; -import {PanelBody} from 'sentry/components/panels/panelBody'; import {IconCheckmark, IconNot} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import type { @@ -321,28 +319,30 @@ function LinkCodeOwners({organization}: {organization: Organization}) { if (integrations.length) { return ( -
+ {t( "Configure code mapping to add your CODEOWNERS file. Select the integration you'd like to use for mapping:" )} -
- + + {integrations.map(integration => ( - {getIntegrationIcon(integration.provider.key)} - {integration.name} + {integration.name} ))} - +
); } return ( -
{t('Install a GitHub or GitLab integration to use this feature.')}
+ + {t('Install a GitHub or GitLab integration to use this feature.')} + {t('Setup Integration')} @@ -354,26 +354,26 @@ function LinkCodeOwners({organization}: {organization: Organization}) { function SourceFile({codeownersFile}: {codeownersFile: CodeownersFile}) { return ( - - + + - {codeownersFile.filepath} + {codeownersFile.filepath} {t('Preview File')} - - + + ); } function NoSourceFile() { return ( - - + + - {t('No codeowner file found.')} - - + {t('No codeowner file found.')} + + ); } @@ -421,30 +421,3 @@ function ErrorMessage({ ); } - -const ResultPanel = styled(Panel)` - margin-bottom: 0; -`; -const NoSourceFileBody = styled(PanelBody)` - display: grid; - padding: 12px; - grid-template-columns: 30px 1fr; - align-items: center; -`; -const SourceFileBody = styled(PanelBody)` - display: grid; - padding: 12px; - grid-template-columns: 30px 1fr auto; - align-items: center; -`; - -const IntegrationsList = styled('div')` - display: grid; - gap: ${p => p.theme.space.md}; - justify-items: center; - margin-top: ${p => p.theme.space.xl}; -`; - -const IntegrationName = styled('p')` - padding-left: 10px; -`; From 0c6d99b86007f61005f49dc455c07239d7678ab3 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:20:54 +0200 Subject: [PATCH 07/16] ref(forms): Use Text as=p in ErrorMessage Replace raw

tags with the scraps Text component per the design system guidelines. --- .../project/projectOwnership/addCodeOwnerModal.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index f32d7d95914f5e..98b6791c0e37a4 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -389,13 +389,17 @@ function ErrorMessage({ errorJSON: {raw?: string} | null; }) { const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); - const errActors = errorJSON?.raw?.[0]!.split('\n').map((el, i) =>

{el}

); + const errActors = errorJSON?.raw?.[0]!.split('\n').map((el, i) => ( + + {el} + + )); return ( {errActors} {codeMapping && ( -

+ {tct( 'Configure [userMappingsLink:User Mappings] or [teamMappingsLink:Team Mappings] for any missing associations.', { @@ -411,7 +415,7 @@ function ErrorMessage({ ), } )} -

+ )} {tct( '[addAndSkip:Add and Skip Missing Associations] will add your codeowner file and skip any rules that having missing associations. You can add associations later for any skipped rules.', From 2f8e45eb087d28fb4270bffd334fbbce997db850 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:22:08 +0200 Subject: [PATCH 08/16] ref(forms): Lift shared Container out of file panels SourceFile and NoSourceFile each wrapped their grid in an identical Container. Move the wrapper up into CodeownersFileStatus so the children just render the grid layout. --- .../projectOwnership/addCodeOwnerModal.tsx | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 98b6791c0e37a4..8090aef90240da 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -237,7 +237,13 @@ function CodeownersFileStatus({ return ( - {codeownersFile ? : } + + {codeownersFile ? ( + + ) : ( + + )} + {mutationIsError && mutationError?.responseJSON?.raw ? ( - - - {codeownersFile.filepath} - - {t('Preview File')} - - - + + + {codeownersFile.filepath} + + {t('Preview File')} + + ); } function NoSourceFile() { return ( - - - - {t('No codeowner file found.')} - - + + + {t('No codeowner file found.')} + ); } From f73115b074bfdcb0dd9243ed3e74758bbac8abb3 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:23:30 +0200 Subject: [PATCH 09/16] test(forms): Wait for Add File button to be enabled before clicking Replace findByRole on the always-present button with an explicit waitFor on the enabled state, since what we actually need to wait for is the codeownersFile mock to resolve. --- .../project/projectOwnership/addCodeOwnerModal.spec.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx index cde5cc3a3c6d97..129e6064a70fb9 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.spec.tsx @@ -161,7 +161,10 @@ describe('AddCodeOwnerModal', () => { }) ); - await userEvent.click(await screen.findByRole('button', {name: 'Add File'})); + await waitFor(() => { + expect(screen.getByRole('button', {name: 'Add File'})).toBeEnabled(); + }); + await userEvent.click(screen.getByRole('button', {name: 'Add File'})); await waitFor(() => { expect(addFileRequest).toHaveBeenCalledWith( From e3fd1198748bf2422180409d52599c819572bbc0 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:43:09 +0200 Subject: [PATCH 10/16] ref(forms): Use space.lg for file panel padding space.md is 8px in scraps but the legacy panel used 12px. Use lg (12px) to match the original spacing. --- .../settings/project/projectOwnership/addCodeOwnerModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 8090aef90240da..c0b19df9576cdf 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -237,7 +237,7 @@ function CodeownersFileStatus({ return ( - + {codeownersFile ? ( ) : ( From c605a761da779a01e57146c8851885885eb1b263 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 10:47:51 +0200 Subject: [PATCH 11/16] ref(forms): Tighten mutation error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Check err instanceof RequestError and fall back to a generic toast for unexpected error shapes, matching the form-migration reference pattern. - Drop gcTime: 0 — redundant for a component-scoped useMutation with no mutationKey. --- .../project/projectOwnership/addCodeOwnerModal.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index c0b19df9576cdf..a68347b4bfdbd3 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -31,7 +31,7 @@ import type {Project} from 'sentry/types/project'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; import {fetchMutation} from 'sentry/utils/queryClient'; -import type {RequestError} from 'sentry/utils/requestError/requestError'; +import {RequestError} from 'sentry/utils/requestError/requestError'; type Props = { organization: Organization; @@ -138,6 +138,10 @@ function ApplyCodeMappings({ data: payload, }), onError: err => { + if (!(err instanceof RequestError)) { + addErrorMessage(t('Something went wrong')); + return; + } if (err.responseJSON && !('raw' in err.responseJSON)) { addErrorMessage( Object.values(err.responseJSON ?? {}) @@ -146,7 +150,6 @@ function ApplyCodeMappings({ ); } }, - gcTime: 0, }); const form = useScrapsForm({ From ff5b4db0940c5dc9fad1a81fd01dfdda7ec21004 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 11:00:39 +0200 Subject: [PATCH 12/16] ref(forms): Drop unsafe cast on ErrorMessage prop Narrow mutationError.responseJSON.raw via typeof at the call site and pass it as a string prop to ErrorMessage. Replaces the as {raw?: string} cast that didn't match RequestError.responseJSON's actual unknown shape. --- .../project/projectOwnership/addCodeOwnerModal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index a68347b4bfdbd3..a5663d34cfc79e 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -237,6 +237,7 @@ function CodeownersFileStatus({ organization: Organization; }) { const {data: codeownersFile} = useCodeownersFile(organization, codeMappingId); + const rawError = mutationError?.responseJSON?.raw; return ( @@ -247,12 +248,12 @@ function CodeownersFileStatus({ )} - {mutationIsError && mutationError?.responseJSON?.raw ? ( + {mutationIsError && typeof rawError === 'string' ? ( ) : null} @@ -386,15 +387,15 @@ function ErrorMessage({ baseUrl, codeMappingId, codeMappings, - errorJSON, + rawError, }: { baseUrl: string; codeMappingId: string | null; codeMappings: RepositoryProjectPathConfig[]; - errorJSON: {raw?: string} | null; + rawError: string; }) { const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); - const errActors = errorJSON?.raw?.[0]!.split('\n').map((el, i) => ( + const errActors = rawError[0]!.split('\n').map((el, i) => ( {el} From e2d76023b1bf3d64b13685a13d2d2fd261e6dae3 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Mon, 11 May 2026 11:03:21 +0200 Subject: [PATCH 13/16] fix(forms): Treat codeowners error raw as string array DRF returns serializer.errors as Record, so responseJSON.raw is string[], not string. The legacy cast and my recent typeof string guard were both incorrect; with the typeof guard the inline ErrorMessage would never render. Narrow via Array.isArray + typeof rawError[0] === 'string', pass the first error message to ErrorMessage, and drop the rawError[0]! indexing inside ErrorMessage so we split the actual message string rather than its first character. --- .../project/projectOwnership/addCodeOwnerModal.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index a5663d34cfc79e..79ac28acb8d105 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -238,6 +238,8 @@ function CodeownersFileStatus({ }) { const {data: codeownersFile} = useCodeownersFile(organization, codeMappingId); const rawError = mutationError?.responseJSON?.raw; + const firstRawError = + Array.isArray(rawError) && typeof rawError[0] === 'string' ? rawError[0] : undefined; return ( @@ -248,12 +250,12 @@ function CodeownersFileStatus({ )} - {mutationIsError && typeof rawError === 'string' ? ( + {mutationIsError && firstRawError ? ( ) : null} @@ -395,7 +397,7 @@ function ErrorMessage({ rawError: string; }) { const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); - const errActors = rawError[0]!.split('\n').map((el, i) => ( + const errActors = rawError.split('\n').map((el, i) => ( {el} From fd28d88051129a02148f53ae3ec874173d4a2bd2 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 12 May 2026 08:23:39 +0200 Subject: [PATCH 14/16] Update static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dominik Dorfmeister 🔮 --- .../project/projectOwnership/addCodeOwnerModal.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 79ac28acb8d105..ec7ccbaeb33a9d 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -125,13 +125,9 @@ function ApplyCodeMappings({ }) { const defaultValues: FormValues = {codeMappingId: null}; - const mutation = useMutation< - CodeOwner, - RequestError, - {codeMappingId: string; raw: string} - >({ - mutationFn: payload => - fetchMutation({ + const mutation = useMutation({ + mutationFn: (payload: {codeMappingId: string; raw: string}) => + fetchMutation({ method: 'POST', url: `/projects/${organization.slug}/${project.slug}/codeowners/`, options: {}, From b52357bf5fddb9e94f474f3eeeec6a4ee24c8317 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 12 May 2026 08:50:42 +0200 Subject: [PATCH 15/16] ref: submit mutation via form.SubmitButton --- .../projectOwnership/addCodeOwnerModal.tsx | 106 +++++------------- 1 file changed, 30 insertions(+), 76 deletions(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index ec7ccbaeb33a9d..d33fe7eddee908 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,15 +1,10 @@ import {Fragment} from 'react'; -import { - skipToken, - useMutation, - useQuery, - type UseMutationResult, -} from '@tanstack/react-query'; +import {skipToken, useMutation, useQuery} from '@tanstack/react-query'; import {z} from 'zod'; import {Alert} from '@sentry/scraps/alert'; -import {Button, LinkButton} from '@sentry/scraps/button'; -import {defaultFormOptions, useScrapsForm} from '@sentry/scraps/form'; +import {LinkButton} from '@sentry/scraps/button'; +import {defaultFormOptions, useScrapsForm, useStore} from '@sentry/scraps/form'; import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; @@ -152,8 +147,24 @@ function ApplyCodeMappings({ ...defaultFormOptions, defaultValues, validators: {onDynamic: schema}, + onSubmit: ({value}) => { + if (!value.codeMappingId || !codeownersFile) { + return; + } + return mutation + .mutateAsync({codeMappingId: value.codeMappingId, raw: codeownersFile.raw}) + .then(data => { + const codeMapping = codeMappings.find(cm => cm.id === value.codeMappingId); + onSave?.({...data, codeMapping}); + closeModal(); + }) + .catch(() => {}); + }, }); + const codeMappingId = useStore(form.store, state => state.values.codeMappingId); + const {data: codeownersFile} = useCodeownersFile(organization, codeMappingId); + return (
{t('Add Code Owner File')}
@@ -174,32 +185,18 @@ function ApplyCodeMappings({ )} - state.values.codeMappingId}> - {codeMappingId => ( - - )} - +
- state.values.codeMappingId}> - {codeMappingId => ( - - )} - + {t('Add File')}
); @@ -222,17 +219,18 @@ function useCodeownersFile(organization: Organization, codeMappingId: string | n function CodeownersFileStatus({ codeMappingId, codeMappings, + codeownersFile, organization, mutationError, mutationIsError, }: { codeMappingId: string | null; codeMappings: RepositoryProjectPathConfig[]; + codeownersFile: CodeownersFile | undefined; mutationError: RequestError | null; mutationIsError: boolean; organization: Organization; }) { - const {data: codeownersFile} = useCodeownersFile(organization, codeMappingId); const rawError = mutationError?.responseJSON?.raw; const firstRawError = Array.isArray(rawError) && typeof rawError[0] === 'string' ? rawError[0] : undefined; @@ -258,50 +256,6 @@ function CodeownersFileStatus({ ); } -function AddFileButton({ - codeMappingId, - codeMappings, - organization, - mutation, - onSave, - closeModal, -}: { - closeModal: ModalRenderProps['closeModal']; - codeMappingId: string | null; - codeMappings: RepositoryProjectPathConfig[]; - mutation: UseMutationResult< - CodeOwner, - RequestError, - {codeMappingId: string; raw: string} - >; - onSave: ((data: CodeOwner) => void) | undefined; - organization: Organization; -}) { - const {data: codeownersFile} = useCodeownersFile(organization, codeMappingId); - - const addFile = () => { - if (!codeownersFile || !codeMappingId) { - return; - } - mutation.mutate( - {codeMappingId, raw: codeownersFile.raw}, - { - onSuccess: data => { - const codeMapping = codeMappings.find(mapping => mapping.id === codeMappingId); - onSave?.({...data, codeMapping}); - closeModal(); - }, - } - ); - }; - - return ( - - ); -} - function LinkCodeOwners({organization}: {organization: Organization}) { const baseUrl = `/settings/${organization.slug}/integrations/`; From b6a59022736712941547ce76d55e9e76822a93a5 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 12 May 2026 09:34:17 +0200 Subject: [PATCH 16/16] fix(forms): narrow mutation.error to RequestError --- .../settings/project/projectOwnership/addCodeOwnerModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index d33fe7eddee908..fff4578b5a41d1 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -190,7 +190,7 @@ function ApplyCodeMappings({ codeMappings={codeMappings} codeownersFile={codeownersFile} organization={organization} - mutationError={mutation.error} + mutationError={mutation.error instanceof RequestError ? mutation.error : null} mutationIsError={mutation.isError} />