Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions static/app/views/projectInstall/createProject.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,54 @@ describe('CreateProject', () => {
expect(screen.getByPlaceholderText('project-slug')).toHaveValue('another');
});

it('should not overwrite a user-entered project name when the name happens to match the current platform key', async () => {
// Regression test: previously, the check `projectName !== platform.key` would incorrectly
// treat the name as auto-generated if it matched the current platform slug, causing a
// platform switch to overwrite a name the user explicitly typed.
const {organization} = initializeOrg({
organization: {
access: ['project:read'],
features: ['team-roles'],
allowMemberProjectCreation: true,
},
});

render(<CreateProject />, {organization});

// User explicitly types a name that happens to match a platform id
await userEvent.type(screen.getByPlaceholderText('project-slug'), 'apple-ios');

// User then selects a different platform
await userEvent.click(screen.getByTestId('platform-ruby-rails'));

// The name they typed should be preserved, not replaced with 'ruby-rails'
expect(screen.getByPlaceholderText('project-slug')).toHaveValue('apple-ios');
});

it('should allow platform to fill the project name again after the user clears it', async () => {
const {organization} = initializeOrg({
organization: {
access: ['project:read'],
features: ['team-roles'],
allowMemberProjectCreation: true,
},
});

render(<CreateProject />, {organization});

// User types a name
await userEvent.type(screen.getByPlaceholderText('project-slug'), 'my-project');
expect(screen.getByPlaceholderText('project-slug')).toHaveValue('my-project');

// User clears the field (signals they want the platform to drive the name again)
await userEvent.clear(screen.getByPlaceholderText('project-slug'));
expect(screen.getByPlaceholderText('project-slug')).toHaveValue('');

// Now selecting a platform should fill the name
await userEvent.click(screen.getByTestId('platform-apple-ios'));
expect(screen.getByPlaceholderText('project-slug')).toHaveValue('apple-ios');
});

it('should display success message on proj creation', async () => {
const {organization} = initializeOrg({
organization: {
Expand Down
15 changes: 10 additions & 5 deletions static/app/views/projectInstall/createProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export function CreateProject() {

const [formData, setFormData] = useState<FormData>(initialData);
const pickerKeyRef = useRef<'create-project' | 'auto-fill'>('create-project');
const hasUserModifiedProjectName = useRef(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ref not reset when useEffect clobbers project name

Medium Severity

The hasUserModifiedProjectName ref can desync from actual form state. The existing useEffect (lines 247–251) resets formData.projectName to '' whenever initialData changes (e.g., when defaultTeam updates due to async team loading), but does not reset hasUserModifiedProjectName. After this, the ref is true while the project name is empty, so subsequent platform selection preserves the empty string instead of auto-filling the platform name — a regression from the old behavior which would auto-fill for empty names.

Additional Locations (2)

Fix in Cursor Fix in Web


const canCreateTeam = organization.access.includes('project:admin');
const isOrgMemberWithNoAccess = accessTeams.length === 0 && !canCreateTeam;
Expand Down Expand Up @@ -442,13 +443,11 @@ export function CreateProject() {
key: value.id,
});

const userModifiedName =
!!formData.projectName && formData.projectName !== formData.platform?.key;
const newName = userModifiedName ? formData.projectName : value.id;
const newName = hasUserModifiedProjectName.current ? formData.projectName : value.id;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoFill project name overwritten on platform change

Low Severity

When autoFill is active (user navigated back from getting-started), the initialData pre-fills projectName from the previously created project (e.g., "my-cool-app"). However, hasUserModifiedProjectName starts as false and is only set true by typing in the input — not by programmatic pre-filling. So if the user changes the platform, handlePlatformChange treats the auto-filled name as auto-generated and overwrites it with the new platform key. The old heuristic (projectName !== platform.key) would have preserved it since the project name typically differs from the platform key.

Additional Locations (1)

Fix in Cursor Fix in Web


updateFormData('projectName', newName);
},
[updateFormData, formData.projectName, formData.platform?.key, organization]
[updateFormData, formData.projectName, organization]
);

const platform = formData.platform?.key;
Expand Down Expand Up @@ -520,7 +519,13 @@ export function CreateProject() {
placeholder={t('project-slug')}
autoComplete="off"
value={formData.projectName}
onChange={e => updateFormData('projectName', slugify(e.target.value))}
onChange={e => {
const slugified = slugify(e.target.value);
// Track whether the user has intentionally set a custom name.
// Reset if they clear the field so platform selection can fill it in again.
hasUserModifiedProjectName.current = slugified !== '';
updateFormData('projectName', slugified);
}}
/>
</ProjectNameInputWrap>
</div>
Expand Down
Loading