diff --git a/rtl-spec/components/version-select.spec.tsx b/rtl-spec/components/version-select.spec.tsx index 85875e4b32..64755b9cd6 100644 --- a/rtl-spec/components/version-select.spec.tsx +++ b/rtl-spec/components/version-select.spec.tsx @@ -12,6 +12,7 @@ import { import { VersionSelect, filterItems, + getItemDisplayText, getItemIcon, getItemLabel, renderItem, @@ -78,11 +79,54 @@ describe('VersionSelect component', () => { const { queryAllByTestId } = render(item); + expect(queryAllByTestId('disabled-menu-item')).toHaveLength(0); + }); + it('does not disable local versions even when disableDownload returns true', () => { + vi.mocked(disableDownload).mockReturnValueOnce(true); + + const localVersion: RunnableVersion = { + ...mockVersion1, + source: local, + state: installed, + name: 'My Build', + }; + + const item = renderItem(localVersion, { + handleClick: () => ({}), + index: 0, + modifiers: { active: true, disabled: false, matchesPredicate: true }, + query: '', + })!; + + const { queryAllByTestId } = render(item); + expect(queryAllByTestId('disabled-menu-item')).toHaveLength(0); }); }); - describe('getItemLabel()', () => { + describe('getItemDisplayText()', () => { + it('returns name for local versions', () => { + const input: RunnableVersion = { + ...mockVersion1, + source: local, + state: installed, + name: 'My Debug Build', + }; + expect(getItemDisplayText(input)).toBe('My Debug Build'); + }); + + it('returns "Local Build" for local versions without a name', () => { + const input: RunnableVersion = { + ...mockVersion1, + source: local, + state: installed, + }; + expect(getItemDisplayText(input)).toBe('Local Build'); + }); + + it('returns version string for remote versions', () => { + expect(getItemDisplayText(mockVersion1)).toBe(mockVersion1.version); + }); it('returns the correct label for an available local version', () => { const input: RunnableVersion = { ...mockVersion1, @@ -90,8 +134,7 @@ describe('VersionSelect component', () => { source: local, }; - expect(getItemLabel(input)).toBe('Local'); - expect(getItemLabel({ ...input, name: 'Hi' })).toBe('Hi'); + expect(getItemLabel(input)).toBe('Local Build'); }); it('returns the correct label for an unavailable local version', () => { @@ -167,6 +210,41 @@ describe('VersionSelect component', () => { expect(filterItems('3', versions)).toEqual(expected); }); + it('sorts local versions before remote versions', () => { + const localVer = { + version: '0.0.0-local.123', + source: local, + name: 'My Build', + } as RunnableVersion; + + const versions = [ + { version: '14.3.0' }, + localVer, + { version: '3.0.0' }, + ] as RunnableVersion[]; + + const result = filterItems('build', versions); + // Only the local version matches 'build' (via name) + expect(result).toEqual([localVer]); + }); + + it('matches local versions by name', () => { + const localVer = { + version: '0.0.0-local.123', + source: local, + name: 'My Debug Build', + } as RunnableVersion; + + const versions = [ + { version: '14.3.0' }, + localVer, + { version: '3.0.0' }, + ] as RunnableVersion[]; + + const result = filterItems('debug', versions); + expect(result).toEqual([localVer]); + }); + it('sorts in descending order when the query is non-numeric', () => { const versions = [ { version: '3.0.0' }, diff --git a/src/less/components/commands.less b/src/less/components/commands.less index 9f2b576fa5..68f220a072 100644 --- a/src/less/components/commands.less +++ b/src/less/components/commands.less @@ -8,12 +8,12 @@ header { background-color: @background-3; -webkit-app-region: drag; - #version-chooser .bp3-button-text::before { + #version-chooser:not([data-local]) .bp3-button-text::before { content: 'Electron v'; } @media (max-width: 980px) { - #version-chooser .bp3-button-text::before { + #version-chooser:not([data-local]) .bp3-button-text::before { content: 'v'; } } diff --git a/src/less/components/settings-electron.less b/src/less/components/settings-electron.less index 46fcc6dcf4..d2c11d1798 100644 --- a/src/less/components/settings-electron.less +++ b/src/less/components/settings-electron.less @@ -11,6 +11,33 @@ overflow: hidden; background-color: @background-1; + .electron-versions-section-header { + padding: 8px 15px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + color: @text-color-2; + background-color: @background-2; + border-bottom: 1px solid @border-color-1; + border-top: 1px solid @border-color-1; + display: flex; + align-items: center; + gap: 6px; + + &:first-child { + border-top: none; + } + + &.local { + border-left: 3px solid @green4; + } + + &.remote { + border-left: 3px solid @blue4; + } + } + .electron-versions-header { display: flex; font-weight: 600; @@ -62,6 +89,14 @@ background-color: rgba(92, 112, 128, 0.15); } + &.local { + border-left: 3px solid @green4; + } + + &.remote { + border-left: 3px solid @blue4; + } + .version-col { flex: 1; padding: 0 15px; @@ -103,6 +138,13 @@ color: @foreground-1; } + .electron-versions-section-header { + background-color: rgba(255, 255, 255, 0.06); + border-bottom-color: @dark-gray4; + border-top-color: @dark-gray4; + color: rgba(255, 255, 255, 0.7); + } + .electron-versions-header { background-color: @dark-gray5; border-bottom-color: @dark-gray4; diff --git a/src/renderer/components/dialog-add-version.tsx b/src/renderer/components/dialog-add-version.tsx index 120b9d7be1..e8ed8aad72 100644 --- a/src/renderer/components/dialog-add-version.tsx +++ b/src/renderer/components/dialog-add-version.tsx @@ -9,7 +9,6 @@ import { Intent, } from '@blueprintjs/core'; import { observer } from 'mobx-react'; -import * as semver from 'semver'; import { Version } from '../../interfaces'; import { AppState } from '../state'; @@ -22,11 +21,20 @@ interface AddVersionDialogProps { interface AddVersionDialogState { isValidElectron: boolean; - isValidVersion: boolean; + isValidName: boolean; existingLocalVersion?: Version; folderPath?: string; localName?: string; - version: string; + name: string; +} + +/** + * Generate a unique version key for a local build. + * Uses a format that is valid semver but can never conflict + * with a real Electron release. + */ +function generateLocalVersionKey(): string { + return `0.0.0-local.${Date.now()}`; } /** @@ -41,14 +49,14 @@ export const AddVersionDialog = observer( super(props); this.state = { - isValidVersion: false, + isValidName: false, isValidElectron: false, - version: '', + name: '', }; this.onSubmit = this.onSubmit.bind(this); this.onClose = this.onClose.bind(this); - this.onChangeVersion = this.onChangeVersion.bind(this); + this.onChangeName = this.onChangeName.bind(this); } /** @@ -65,20 +73,23 @@ export const AddVersionDialog = observer( folderPath, isValidElectron, localName, + // Pre-fill name from detected binary name if available + name: localName || '', + isValidName: !!localName, }); } } /** - * Handles a change of the file input + * Handles a change of the name input */ - public onChangeVersion(event: React.ChangeEvent) { - const version = event.target.value || ''; - const isValidVersion = !!semver.valid(version); + public onChangeName(event: React.ChangeEvent) { + const name = event.target.value || ''; + const isValidName = name.trim().length > 0; this.setState({ - version, - isValidVersion, + name, + isValidName, }); } @@ -86,27 +97,21 @@ export const AddVersionDialog = observer( * Handles the submission of the dialog */ public async onSubmit(): Promise { - const { - folderPath, - version, - isValidElectron, - existingLocalVersion, - localName, - } = this.state; + const { folderPath, name, isValidElectron, existingLocalVersion } = + this.state; if (!folderPath) return; - const toAdd: Version = { - localPath: folderPath, - version, - name: localName, - }; - // swap to old local electron version if the user adds a new one with the same path if (isValidElectron && existingLocalVersion?.localPath) { // set previous version as active version this.props.appState.setVersion(existingLocalVersion.version); } else { + const toAdd: Version = { + localPath: folderPath, + version: generateLocalVersionKey(), + name: name.trim(), + }; this.props.appState.addLocalVersion(toAdd); } this.onClose(); @@ -121,9 +126,8 @@ export const AddVersionDialog = observer( } get buttons() { - const { isValidElectron, isValidVersion, existingLocalVersion } = - this.state; - const canAdd = isValidElectron && isValidVersion && !existingLocalVersion; + const { isValidElectron, isValidName, existingLocalVersion } = this.state; + const canAdd = isValidElectron && isValidName && !existingLocalVersion; const canSwitch = isValidElectron && existingLocalVersion; return [ @@ -209,20 +213,20 @@ export const AddVersionDialog = observer( } private renderVersionInput(): JSX.Element | null { - const { isValidElectron, isValidVersion, version } = this.state; + const { isValidElectron, isValidName, name } = this.state; if (!isValidElectron) return null; return ( <>

- Please specify a version, used for typings and the name. Must be{' '} - semver compliant. + Give this local build a name so you can identify it in the version + list.

); @@ -234,8 +238,8 @@ export const AddVersionDialog = observer( private reset(): void { this.setState({ isValidElectron: false, - isValidVersion: false, - version: '', + isValidName: false, + name: '', folderPath: undefined, localName: undefined, }); diff --git a/src/renderer/components/settings-electron.tsx b/src/renderer/components/settings-electron.tsx index 87fa84f1c6..782eb25f2c 100644 --- a/src/renderer/components/settings-electron.tsx +++ b/src/renderer/components/settings-electron.tsx @@ -119,7 +119,7 @@ const ElectronVersionRow = observer(({ index, style, data }: RowProps) => { /> ); - } else if (disableDownload(version)) { + } else if (!isLocal && disableDownload(version)) { return ( { return