Skip to content

Commit 6972da9

Browse files
maxyingerclaude
andcommitted
refactor(ui): never save an empty username
Drops `isUsernameRequired`. The legacy form let an empty value through on an instance where the username is optional, which is how it doubled as "remove my username" — an incidental consequence of its submit gate, not an affordance anything offers. The new surface does not offer it, so the rule is unconditional and the prop that carried it is gone. Back to the two-prop public surface: `username` and `onSaveUsername`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nA7S7H8uK8qPzwm1PwzoR
1 parent 2bcbb86 commit 6972da9

6 files changed

Lines changed: 13 additions & 61 deletions

File tree

‎packages/swingset/src/stories/fixtures/user-profile-edit-username.ts‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class FormError extends Error {
1313

1414
export interface UserProfileEditUsernameFixtureOptions {
1515
username?: string;
16-
isRequired?: boolean;
1716
latency?: number;
1817
/** Rejects every save instead of committing it. */
1918
failWith?: UserProfileFormError;
@@ -22,15 +21,13 @@ export interface UserProfileEditUsernameFixtureOptions {
2221
/** Stands in for the model. Everything else the dialog needs belongs to the controller. */
2322
export function useUserProfileEditUsernameFixture({
2423
username: initialUsername = 'prestonxyz',
25-
isRequired = false,
2624
latency = 800,
2725
failWith,
2826
}: UserProfileEditUsernameFixtureOptions = {}) {
2927
const [username, setUsername] = useState(initialUsername);
3028

3129
return {
3230
username,
33-
isUsernameRequired: isRequired,
3431
onSaveUsername: async (value: string) => {
3532
await new Promise(resolve => setTimeout(resolve, latency));
3633
if (failWith) {

‎packages/swingset/src/stories/user-profile-account-section.stories.tsx‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,12 @@ export const meta: StoryMeta = {
2424

2525
function AccountSection({
2626
allowMultipleAccounts,
27-
isUsernameRequired,
2827
failWith,
2928
}: {
3029
allowMultipleAccounts: boolean;
31-
isUsernameRequired?: boolean;
3230
failWith?: UserProfileFormError;
3331
}) {
34-
const editUsername = useUserProfileEditUsernameFixture({ isRequired: isUsernameRequired, failWith });
32+
const editUsername = useUserProfileEditUsernameFixture({ failWith });
3533
const [emails, setEmails] = useState<UserProfileEmail[]>(
3634
allowMultipleAccounts
3735
? [
@@ -101,13 +99,3 @@ export function EditUsernameFails() {
10199
/>
102100
);
103101
}
104-
105-
/** A username the instance insists on, so clearing the field leaves the action inert. */
106-
export function UsernameRequired() {
107-
return (
108-
<AccountSection
109-
allowMultipleAccounts={false}
110-
isUsernameRequired
111-
/>
112-
);
113-
}

‎packages/ui/src/mosaic/user-profile/__tests__/user-profile-edit-username.controller.test.ts‎

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,9 @@ describe('userProfileEditUsernameMachine', () => {
8989
expect(saveUsername).not.toHaveBeenCalled();
9090
});
9191

92-
it('refuses to save an empty value where a username is required', () => {
92+
it('refuses to save an empty value, since clearing a username is not on offer', () => {
9393
const saveUsername = vi.fn(() => Promise.resolve());
94-
const actor = createActor(userProfileEditUsernameMachine, {
95-
context: { saveUsername, savedUsername: 'prestonxyz', isRequired: true },
96-
}).start();
97-
actor.send({ type: 'OPEN' });
94+
const actor = start(saveUsername);
9895
actor.send({ type: 'TYPE', value: '' });
9996

10097
actor.send({ type: 'SAVE' });
@@ -161,25 +158,14 @@ describe('useUserProfileEditUsernameController', () => {
161158
expect(result.current.canSave).toBe(true);
162159
});
163160

164-
it('withholds the save on an empty value only where a username is required', () => {
165-
const optional = renderHook(() =>
161+
it('withholds the save on an empty value', () => {
162+
const { result } = renderHook(() =>
166163
useUserProfileEditUsernameController({ username: 'prestonxyz', onSave: () => Promise.resolve() }),
167164
);
168-
const required = renderHook(() =>
169-
useUserProfileEditUsernameController({
170-
username: 'prestonxyz',
171-
isRequired: true,
172-
onSave: () => Promise.resolve(),
173-
}),
174-
);
175165

176-
act(() => optional.result.current.onOpenChange(true));
177-
act(() => optional.result.current.onUsernameChange(''));
178-
act(() => required.result.current.onOpenChange(true));
179-
act(() => required.result.current.onUsernameChange(''));
166+
act(() => result.current.onOpenChange(true));
167+
act(() => result.current.onUsernameChange(''));
180168

181-
// Clearing an optional username is a real edit; clearing a required one is not.
182-
expect(optional.result.current.canSave).toBe(true);
183-
expect(required.result.current.canSave).toBe(false);
169+
expect(result.current.canSave).toBe(false);
184170
});
185171
});

‎packages/ui/src/mosaic/user-profile/user-profile-account-section.view.tsx‎

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ export interface UserProfileAccountSectionViewProps {
4646
hasImage?: boolean;
4747
name: string;
4848
username: string;
49-
/** Whether the instance refuses an empty username. From `userSettings.attributes.username.required`. */
50-
isUsernameRequired?: boolean;
5149
emails: UserProfileEmail[];
5250
phones: UserProfilePhone[];
5351
onProfilePictureChange?: (file: File) => void;
@@ -59,7 +57,6 @@ export interface UserProfileAccountSectionViewProps {
5957
onProfilePictureReject?: (rejections: FileRejection[]) => void;
6058
onRemoveProfilePicture?: () => void;
6159
onNameChange?: (value: string) => void;
62-
/** Resolve to close the dialog; reject with an `Error` to keep it open showing why. Omit to hide the action. */
6360
onSaveUsername?: (username: string) => Promise<void>;
6461
onAddEmail?: () => void;
6562
onManageEmail?: (id: string) => void;
@@ -79,7 +76,6 @@ export function UserProfileAccountSectionView({
7976
hasImage = false,
8077
name,
8178
username,
82-
isUsernameRequired,
8379
emails,
8480
phones,
8581
onProfilePictureChange,
@@ -179,7 +175,6 @@ export function UserProfileAccountSectionView({
179175
{onSaveUsername ? (
180176
<Section.Actions>
181177
<EditUsername
182-
isRequired={isUsernameRequired}
183178
username={username}
184179
onSave={onSaveUsername}
185180
/>
@@ -292,16 +287,8 @@ function ProfilePictureActions({
292287
}
293288

294289
/** Split out so the controller is mounted only where the action exists. */
295-
function EditUsername({
296-
isRequired,
297-
username,
298-
onSave,
299-
}: {
300-
isRequired?: boolean;
301-
username: string;
302-
onSave: (username: string) => Promise<void>;
303-
}) {
304-
const controller = useUserProfileEditUsernameController({ username, isRequired, onSave });
290+
function EditUsername({ username, onSave }: { username: string; onSave: (username: string) => Promise<void> }) {
291+
const controller = useUserProfileEditUsernameController({ username, onSave });
305292

306293
return (
307294
<UserProfileEditUsernameView

‎packages/ui/src/mosaic/user-profile/user-profile-edit-username.controller.ts‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export interface UserProfileEditUsernameContext {
77
saveUsername: (username: string) => Promise<void>;
88
/** Injected every render. What `OPEN` seeds the field from. */
99
savedUsername: string;
10-
isRequired: boolean;
1110
username: string;
1211
error: UserProfileFormError<UserProfileEditUsernameField> | undefined;
1312
}
@@ -24,9 +23,9 @@ function notSeated(): Promise<never> {
2423
return Promise.reject(new Error('edit-username deps are not seated'));
2524
}
2625

27-
/** The legacy form's two rules: nothing to save until the value moves, and no empty value where the instance requires one. */
26+
/** Clearing a username is not something the surface offers, so an empty value is never saveable. */
2827
function isSaveable(context: UserProfileEditUsernameContext): boolean {
29-
return context.username !== context.savedUsername && !(context.isRequired && context.username === '');
28+
return context.username !== context.savedUsername && context.username !== '';
3029
}
3130

3231
type FieldCopy = UserProfileFormError<UserProfileEditUsernameField>['fields'];
@@ -44,7 +43,6 @@ export const userProfileEditUsernameMachine = createMachine({
4443
context: {
4544
saveUsername: notSeated,
4645
savedUsername: '',
47-
isRequired: false,
4846
username: '',
4947
error: undefined,
5048
},
@@ -78,7 +76,6 @@ export const userProfileEditUsernameMachine = createMachine({
7876

7977
export interface UserProfileEditUsernameControllerOptions {
8078
username?: string;
81-
isRequired?: boolean;
8279
onSave: (username: string) => Promise<void>;
8380
}
8481

@@ -95,11 +92,10 @@ export interface UserProfileEditUsernameController {
9592

9693
export function useUserProfileEditUsernameController({
9794
username = '',
98-
isRequired = false,
9995
onSave,
10096
}: UserProfileEditUsernameControllerOptions): UserProfileEditUsernameController {
10197
const [snapshot, send] = useMachine(userProfileEditUsernameMachine, {
102-
context: { saveUsername: onSave, savedUsername: username, isRequired },
98+
context: { saveUsername: onSave, savedUsername: username },
10399
});
104100

105101
return {

‎packages/ui/src/mosaic/user-profile/user-profile-profile-panel.view.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export function UserProfileProfilePanelView({
3838
hasImage,
3939
name = '',
4040
username = '',
41-
isUsernameRequired,
4241
emails = [],
4342
phones = [],
4443
connectedAccounts = [],
@@ -76,7 +75,6 @@ export function UserProfileProfilePanelView({
7675
emails={emails}
7776
hasImage={hasImage}
7877
imageUrl={imageUrl}
79-
isUsernameRequired={isUsernameRequired}
8078
name={name}
8179
phones={phones}
8280
username={username}

0 commit comments

Comments
 (0)