Skip to content

Commit 938d777

Browse files
Merge remote-tracking branch 'origin/master' into jb/ref/migrate-personal-tokens-form
# Conflicts: # static/app/utils/useMutateApiToken.tsx # static/app/views/settings/account/apiNewToken.spec.tsx # static/app/views/settings/account/apiNewToken.tsx # static/app/views/settings/account/apiTokenDetails.tsx
2 parents fd8d34f + 7585e12 commit 938d777

File tree

6,925 files changed

+186997
-88341
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

6,925 files changed

+186997
-88341
lines changed

.agents/skills/cell-architecture/SKILL.md

Lines changed: 268 additions & 0 deletions
Large diffs are not rendered by default.

.agents/skills/design-system/SKILL.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {Container} from '@sentry/scraps/layout';
5151

5252
// ❌ Don't create styled components
5353
const Component = styled('div')`
54-
padding: ${space(2)};
54+
padding: ${p => p.theme.space.md};
5555
border: 1px solid ${p => p.theme.tokens.border.primary};
5656
`;
5757

@@ -119,7 +119,7 @@ import {Grid} from '@sentry/scraps/layout';
119119
const Component = styled('div')`
120120
display: grid;
121121
grid-template-columns: repeat(3, 1fr);
122-
gap: ${space(2)};
122+
gap: ${p => p.theme.space.md};
123123
`;
124124

125125
// ✅ Use Grid primitive
@@ -147,7 +147,7 @@ import {Stack} from '@sentry/scraps/layout';
147147
const Component = styled('div')`
148148
display: flex;
149149
flex-direction: column;
150-
gap: ${space(2)};
150+
gap: ${p => p.theme.space.md};
151151
`;
152152

153153
// ✅ Use Stack primitive (automatically column direction)
@@ -204,7 +204,7 @@ import {Text} from '@sentry/scraps/text';
204204
// ❌ Don't create styled text components
205205
const Label = styled('span')`
206206
color: ${p => p.theme.tokens.content.secondary};
207-
font-size: ${p => p.theme.fontSizes.small};
207+
font-size: ${p => p.theme.font.size.sm};
208208
`;
209209

210210
// ❌ Don't use raw elements
@@ -242,7 +242,7 @@ import {Heading} from '@sentry/scraps/text';
242242

243243
// ❌ Don't style heading elements
244244
const Title = styled('h2')`
245-
font-size: ${p => p.theme.fontSize.md};
245+
font-size: ${p => p.theme.font.size.md};
246246
font-weight: bold;
247247
`;
248248

@@ -401,7 +401,7 @@ Container supports `margin` props but they are deprecated. Use `gap` on parent c
401401
```tsx
402402
// ❌ Don't use margin between children
403403
const Child = styled('div')`
404-
margin-right: ${p => p.theme.spacing.lg};
404+
margin-right: ${p => p.theme.space.lg};
405405
`;
406406

407407
// ✅ Use gap on parent container
@@ -421,7 +421,7 @@ const Component = styled('div')`
421421
display: flex;
422422
flex-direction: column;
423423
color: ${p => p.theme.tokens.content.secondary};
424-
font-size: ${p => p.theme.fontSize.lg};
424+
font-size: ${p => p.theme.font.size.lg};
425425
`;
426426

427427
// ✅ Split into layout and typography primitives

.agents/skills/generate-frontend-forms/SKILL.md

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This skill provides patterns for building forms using Sentry's new form system b
99

1010
## Core Principle
1111

12-
- Always use the new form system (`useScrapsForm`, `AutoSaveField`) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems.
12+
- Always use the new form system (`useScrapsForm`, `AutoSaveForm`) for new forms. Never create new forms with the legacy JsonForm or Reflux-based systems.
1313

1414
- All forms should be schema based. DO NOT create a form without schema validation.
1515

@@ -21,7 +21,7 @@ All form components are exported from `@sentry/scraps/form`:
2121
import {z} from 'zod';
2222

2323
import {
24-
AutoSaveField,
24+
AutoSaveForm,
2525
defaultFormOptions,
2626
setFieldErrors,
2727
useScrapsForm,
@@ -439,6 +439,44 @@ const userSchema = z.object({
439439
});
440440
```
441441

442+
### Nullable Fields with Refine
443+
444+
When a field starts as `null` (e.g., a required select with no initial selection), use `.nullable().refine()` in the schema. This creates a difference between the schema's _input_ type (which accepts `null`) and its _output_ type (which does not). To handle this correctly:
445+
446+
1. Type `defaultValues` explicitly as `z.input<typeof schema>` — this allows `null` as an initial value.
447+
2. Call `schema.parse(value)` inside `onSubmit` to narrow from `z.input` to `z.output`, stripping the `null` before passing to your mutation.
448+
449+
```tsx
450+
const schema = z.object({
451+
provider: z
452+
.enum(['GitHub', 'LaunchDarkly'])
453+
.nullable()
454+
.refine(v => v !== null, 'Provider is required'),
455+
name: z.string().min(1, 'Name is required'),
456+
});
457+
458+
// z.input allows null for the provider field
459+
const defaultValues: z.input<typeof schema> = {
460+
provider: null,
461+
name: '',
462+
};
463+
464+
// z.output<typeof schema> has provider as non-null after refine
465+
type FormOutput = z.output<typeof schema>;
466+
467+
const form = useScrapsForm({
468+
...defaultFormOptions,
469+
defaultValues,
470+
validators: {onDynamic: schema},
471+
onSubmit: ({value}) => {
472+
// schema.parse narrows null away — mutation receives z.output
473+
return mutation.mutateAsync(schema.parse(value)).catch(() => {});
474+
},
475+
});
476+
```
477+
478+
> **Important**: Do NOT use non-null assertions (`value.provider!`) or type casts to work around nullable fields. The `schema.parse()` approach is both type-safe and validates at runtime.
479+
442480
### Conditional Validation
443481

444482
Use `.refine()` for cross-field validation:
@@ -532,14 +570,14 @@ Validation errors automatically show as a warning icon with tooltip in the field
532570

533571
## Auto-Save Pattern
534572

535-
For settings pages where each field saves independently, use `AutoSaveField`.
573+
For settings pages where each field saves independently, use `AutoSaveForm`.
536574

537-
### Basic Auto-Save Field
575+
### Basic Auto-Save Form
538576

539577
```tsx
540578
import {z} from 'zod';
541579

542-
import {AutoSaveField} from '@sentry/scraps/form';
580+
import {AutoSaveForm} from '@sentry/scraps/form';
543581

544582
import {fetchMutation} from 'sentry/utils/queryClient';
545583

@@ -549,7 +587,7 @@ const schema = z.object({
549587

550588
function SettingsForm() {
551589
return (
552-
<AutoSaveField
590+
<AutoSaveForm
553591
name="displayName"
554592
schema={schema}
555593
initialValue={user.displayName}
@@ -572,7 +610,7 @@ function SettingsForm() {
572610
<field.Input value={field.state.value} onChange={field.handleChange} />
573611
</field.Layout.Row>
574612
)}
575-
</AutoSaveField>
613+
</AutoSaveForm>
576614
);
577615
}
578616
```
@@ -603,7 +641,7 @@ The form system automatically shows:
603641
For dangerous operations (security settings, permissions), use the `confirm` prop to show a confirmation modal before saving. The `confirm` prop accepts either a string or a function.
604642

605643
```tsx
606-
<AutoSaveField
644+
<AutoSaveForm
607645
name="require2FA"
608646
schema={schema}
609647
initialValue={false}
@@ -619,7 +657,7 @@ For dangerous operations (security settings, permissions), use the `confirm` pro
619657
<field.Switch checked={field.state.value} onChange={field.handleChange} />
620658
</field.Layout.Row>
621659
)}
622-
</AutoSaveField>
660+
</AutoSaveForm>
623661
```
624662

625663
**Confirm Config Options:**
@@ -694,6 +732,20 @@ function MyForm() {
694732
}
695733
```
696734

735+
### Resetting After Save
736+
737+
When a form stays on the page after submission (e.g., settings pages), call `form.reset()` after a successful mutation. This re-syncs the form with updated `defaultValues` so it becomes pristine again — any UI that depends on the form being dirty (like conditionally shown Save/Cancel buttons) will update correctly.
738+
739+
```tsx
740+
onSubmit: ({value}) =>
741+
mutation
742+
.mutateAsync(value)
743+
.then(() => form.reset())
744+
.catch(() => {}),
745+
```
746+
747+
> **Note**: `AutoSaveForm` handles this automatically. You only need to add this when using `useScrapsForm`.
748+
697749
### Submit Button
698750

699751
```tsx
@@ -741,6 +793,33 @@ const form = useScrapsForm({
741793
});
742794
```
743795

796+
### Nullable Default Values
797+
798+
```tsx
799+
// ❌ Don't use non-null assertions or type casts
800+
onSubmit: ({value}) => {
801+
return mutation.mutateAsync({...value, provider: value.provider!});
802+
};
803+
804+
// ❌ Don't skip typing defaultValues when the schema has refine
805+
const form = useScrapsForm({
806+
...defaultFormOptions,
807+
defaultValues: {provider: null, name: ''}, // type is inferred but imprecise
808+
});
809+
810+
// ✅ Use z.input for defaultValues and schema.parse in onSubmit
811+
const defaultValues: z.input<typeof schema> = {provider: null, name: ''};
812+
813+
const form = useScrapsForm({
814+
...defaultFormOptions,
815+
defaultValues,
816+
validators: {onDynamic: schema},
817+
onSubmit: ({value}) => {
818+
return mutation.mutateAsync(schema.parse(value)).catch(() => {});
819+
},
820+
});
821+
```
822+
744823
### Form Submissions
745824

746825
```tsx
@@ -763,7 +842,11 @@ onSubmit: ({value}) => {
763842
// Return the promise to keep form.isSubmitting working
764843
// Add .catch(() => {}) to avoid unhandled rejection - error handling
765844
// is done by TanStack Query (onError callback, mutation.isError state)
766-
return mutation.mutateAsync(value).catch(() => {});
845+
// Add .then(() => form.reset()) if the form stays on the page after save
846+
return mutation
847+
.mutateAsync(value)
848+
.then(() => form.reset())
849+
.catch(() => {});
767850
};
768851
```
769852

@@ -803,7 +886,7 @@ mutationOptions={{
803886
mutationFn: (data) => fetchMutation({url: '/user/', method: 'PUT', data}),
804887
onSuccess: (data) => {
805888
queryClient.setQueryData(['user'], old => ({...old, ...data}));
806-
// No toast needed - AutoSaveField shows a checkmark automatically
889+
// No toast needed - AutoSaveForm shows a checkmark automatically
807890
},
808891
}}
809892
```
@@ -850,6 +933,23 @@ const opts = mutationOptions({
850933

851934
Make sure the zod schema's types are compatible with the API type. For example, if the API expects a string union like `'off' | 'low' | 'high'`, use `z.enum(['off', 'low', 'high'])` instead of `z.string()`.
852935

936+
### Form Reset After Save
937+
938+
```tsx
939+
// ❌ Don't forget to reset forms that stay on the page after save
940+
onSubmit: ({value}) => {
941+
return mutation.mutateAsync(value).catch(() => {});
942+
};
943+
944+
// ✅ Call form.reset() after successful save to sync with updated defaultValues
945+
onSubmit: ({value}) => {
946+
return mutation
947+
.mutateAsync(value)
948+
.then(() => form.reset())
949+
.catch(() => {});
950+
};
951+
```
952+
853953
### Layout Choice
854954

855955
```tsx
@@ -869,17 +969,18 @@ When creating a new form:
869969
- [ ] Import from `@sentry/scraps/form` and `zod`
870970
- [ ] Define Zod schema with helpful error messages
871971
- [ ] Use `useScrapsForm` with `...defaultFormOptions`
872-
- [ ] Set `defaultValues` matching schema shape
972+
- [ ] Set `defaultValues` matching schema shape (use `z.input<typeof schema>` if schema has `.refine()`)
873973
- [ ] Set `validators: {onDynamic: schema}`
874974
- [ ] Wrap with `<form.AppForm form={form}>`
875975
- [ ] Use `<form.AppField>` for each field
876976
- [ ] Choose appropriate layout (Stack or Row)
877977
- [ ] Handle server errors with `setFieldErrors`
878978
- [ ] Add `<form.SubmitButton>` for submission
979+
- [ ] Call `form.reset()` after successful mutation if the form stays on the page
879980

880981
When creating auto-save fields:
881982

882-
- [ ] Use `<AutoSaveField>` component
983+
- [ ] Use `<AutoSaveForm>` component
883984
- [ ] Pass `schema` for validation
884985
- [ ] Pass `initialValue` from current data
885986
- [ ] Configure `mutationOptions` with `mutationFn`
@@ -889,10 +990,10 @@ When creating auto-save fields:
889990

890991
## File References
891992

892-
| File | Purpose |
893-
| --------------------------------------------------------- | --------------------------- |
894-
| `static/app/components/core/form/scrapsForm.tsx` | Main form hook |
895-
| `static/app/components/core/form/field/autoSaveField.tsx` | Auto-save wrapper |
896-
| `static/app/components/core/form/field/*.tsx` | Individual field components |
897-
| `static/app/components/core/form/layout/index.tsx` | Layout components |
898-
| `static/app/components/core/form/form.stories.tsx` | Usage examples |
993+
| File | Purpose |
994+
| -------------------------------------------------- | --------------------------- |
995+
| `static/app/components/core/form/scrapsForm.tsx` | Main form hook |
996+
| `static/app/components/core/form/autoSaveForm.tsx` | Auto-save wrapper |
997+
| `static/app/components/core/form/field/*.tsx` | Individual field components |
998+
| `static/app/components/core/form/layout/index.tsx` | Layout components |
999+
| `static/app/components/core/form/form.stories.tsx` | Usage examples |

0 commit comments

Comments
 (0)