mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-27 10:19:26 -05:00
Open spec file from desktop gui and refactor ui-components to typescript (#7747)
This commit is contained in:
@@ -2,56 +2,69 @@
|
||||
"integration": [
|
||||
{
|
||||
"name": "app_spec.coffee",
|
||||
"absolute": "/user/project/cypress/integration/app_spec.coffee",
|
||||
"relative": "cypress/integration/app_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "accounts/account_new_spec.coffee",
|
||||
"absolute": "/user/project/cypress/integration/accounts/account_new_spec.coffee",
|
||||
"relative": "cypress/integration/accounts/account_new_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "accounts/accounts_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/integration/accounts/accounts_list_spec.coffee",
|
||||
"relative": "cypress/integration/accounts/accounts_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin_users_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/integration/admin_users/admin_users_list_spec.coffee",
|
||||
"relative": "cypress/integration/admin_users/admin_users_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin.user/foo_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/integration/admin_users/admin.user/foo_list_spec.coffee",
|
||||
"relative": "cypress/integration/admin_users/admin.user/foo_list_spec.coffee"
|
||||
}
|
||||
],
|
||||
"unit": [
|
||||
{
|
||||
"name": "admin_users/admin/users/bar_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/bar_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/bar_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/one_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/one_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/one_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/two_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/two_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/two_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/three_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/three_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/three_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/four_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/four_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/four_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/five_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/five_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/five_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/six_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/six_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/six_list_spec.coffee"
|
||||
},
|
||||
{
|
||||
"name": "admin_users/admin/users/all/admin/last_list_spec.coffee",
|
||||
"absolute": "/user/project/cypress/unit/admin_users/admin/users/all/admin/last_list_spec.coffee",
|
||||
"relative": "cypress/unit/admin_users/admin/users/all/admin/last_list_spec.coffee"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,12 +16,15 @@ describe('Specs List', function () {
|
||||
cy.stub(this.ipc, 'getOptions').resolves({ projectRoot: '/foo/bar' })
|
||||
cy.stub(this.ipc, 'getCurrentUser').resolves(this.user)
|
||||
cy.stub(this.ipc, 'getSpecs').yields(null, this.specs)
|
||||
cy.stub(this.ipc, 'getUserEditor').resolves({})
|
||||
cy.stub(this.ipc, 'closeBrowser').resolves(null)
|
||||
cy.stub(this.ipc, 'launchBrowser')
|
||||
cy.stub(this.ipc, 'openFinder')
|
||||
cy.stub(this.ipc, 'openFile')
|
||||
cy.stub(this.ipc, 'externalOpen')
|
||||
cy.stub(this.ipc, 'onboardingClosed')
|
||||
cy.stub(this.ipc, 'onSpecChanged')
|
||||
cy.stub(this.ipc, 'setUserEditor')
|
||||
|
||||
this.openProject = this.util.deferred()
|
||||
cy.stub(this.ipc, 'openProject').returns(this.openProject.promise)
|
||||
@@ -173,8 +176,8 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('lists test specs', function () {
|
||||
cy.get('.file a').last().should('contain', 'last_list_spec.coffee')
|
||||
cy.get('.file a').last().should('not.contain', 'admin_users')
|
||||
cy.get('.file .file-name-wrapper').last().should('contain', 'last_list_spec.coffee')
|
||||
cy.get('.file .file-name-wrapper').last().should('not.contain', 'admin_users')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -515,7 +518,7 @@ describe('Specs List', function () {
|
||||
this.ipc.getSpecs.yields(null, this.specs)
|
||||
this.openProject.resolve(this.config)
|
||||
|
||||
cy.contains('.file a', 'app_spec.coffee').as('firstSpec')
|
||||
cy.contains('.file .file-name-wrapper', 'app_spec.coffee').as('firstSpec')
|
||||
})
|
||||
|
||||
it('closes then launches browser on click of file', () => {
|
||||
@@ -533,9 +536,11 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('adds \'active\' class on click', () => {
|
||||
cy.get('@firstSpec')
|
||||
cy.get('@firstSpec').parent()
|
||||
.should('not.have.class', 'active')
|
||||
.click()
|
||||
|
||||
cy.get('@firstSpec').click()
|
||||
.parent()
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
|
||||
@@ -544,7 +549,7 @@ describe('Specs List', function () {
|
||||
this.ipc.getSpecs.yield(null, this.specs)
|
||||
})
|
||||
|
||||
cy.get('@firstSpec').should('have.class', 'active')
|
||||
cy.get('@firstSpec').parent().should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -557,7 +562,7 @@ describe('Specs List', function () {
|
||||
|
||||
context('choose shallow spec', function () {
|
||||
beforeEach(() => {
|
||||
cy.get('.file a').contains('a', 'app_spec.coffee').as('firstSpec').click()
|
||||
cy.get('.file .file-name-wrapper').contains('a', 'app_spec.coffee').as('firstSpec').click()
|
||||
})
|
||||
|
||||
it('updates spec icon', function () {
|
||||
@@ -566,13 +571,13 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('sets spec as active', () => {
|
||||
cy.get('@firstSpec').should('have.class', 'active')
|
||||
cy.get('@firstSpec').parent().should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
|
||||
context('choose deeper nested spec', function () {
|
||||
beforeEach(() => {
|
||||
cy.get('.file a').contains('a', 'last_list_spec.coffee').as('deepSpec').click()
|
||||
cy.get('.file .file-name-wrapper').contains('a', 'last_list_spec.coffee').as('deepSpec').click()
|
||||
})
|
||||
|
||||
it('updates spec icon', () => {
|
||||
@@ -580,7 +585,7 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('sets spec as active', () => {
|
||||
cy.get('@deepSpec').should('have.class', 'active')
|
||||
cy.get('@deepSpec').parent().should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -603,8 +608,8 @@ describe('Specs List', function () {
|
||||
})
|
||||
|
||||
it('updates active spec', function () {
|
||||
cy.get('@firstSpec').should('not.have.class', 'active')
|
||||
cy.get('@secondSpec').should('have.class', 'active')
|
||||
cy.get('@firstSpec').parent().should('not.have.class', 'active')
|
||||
cy.get('@secondSpec').parent().should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -623,15 +628,155 @@ describe('Specs List', function () {
|
||||
this.ipc.onSpecChanged.yield(null, 'integration/app_spec.coffee')
|
||||
})
|
||||
|
||||
cy.get('@firstSpec').should('have.class', 'active')
|
||||
cy.get('@firstSpec').parent().should('have.class', 'active')
|
||||
.then(function () {
|
||||
this.ipc.onSpecChanged.yield(null, 'integration/accounts/account_new_spec.coffee')
|
||||
})
|
||||
|
||||
cy.get('@firstSpec').should('not.have.class', 'active')
|
||||
cy.get('@firstSpec').parent().should('not.have.class', 'active')
|
||||
|
||||
cy.contains('a', 'account_new_spec.coffee')
|
||||
.parent()
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
})
|
||||
|
||||
describe('open in IDE', function () {
|
||||
beforeEach(function () {
|
||||
this.ipc.getSpecs.yields(null, this.specs)
|
||||
|
||||
this.openProject.resolve(this.config)
|
||||
|
||||
cy.get('.file').contains('a', 'app_spec.coffee').parent().as('spec')
|
||||
cy.get('@spec').contains('Open in IDE').as('button')
|
||||
})
|
||||
|
||||
it('does not display button without hover', function () {
|
||||
cy.contains('Open in IDE').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('displays when spec is hovered over', function () {
|
||||
cy.get('@button').invoke('show').should('be.visible')
|
||||
})
|
||||
|
||||
describe('opens files', function () {
|
||||
beforeEach(function () {
|
||||
this.availableEditors = [
|
||||
{ id: 'computer', name: 'On Computer', isOther: false, openerId: 'computer' },
|
||||
{ id: 'atom', name: 'Atom', isOther: false, openerId: 'atom' },
|
||||
{ id: 'vim', name: 'Vim', isOther: false, openerId: 'vim' },
|
||||
{ id: 'sublime', name: 'Sublime Text', isOther: false, openerId: 'sublime' },
|
||||
{ id: 'vscode', name: 'Visual Studio Code', isOther: false, openerId: 'vscode' },
|
||||
{ id: 'other', name: 'Other', isOther: true, openerId: '' },
|
||||
]
|
||||
|
||||
cy.get('@button').invoke('show')
|
||||
})
|
||||
|
||||
context('when user has not already set opener and opens file', function () {
|
||||
beforeEach(function () {
|
||||
this.ipc.getUserEditor.resolves({
|
||||
availableEditors: this.availableEditors,
|
||||
preferredOpener: this.availableEditors[4],
|
||||
})
|
||||
})
|
||||
|
||||
it('opens in preferred opener', function () {
|
||||
cy.get('@button').click().then(() => {
|
||||
expect(this.ipc.openFile).to.be.calledWith({
|
||||
where: this.availableEditors[4],
|
||||
file: '/user/project/cypress/integration/app_spec.coffee',
|
||||
line: 0,
|
||||
column: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when user has not already set opener and opens file', function () {
|
||||
beforeEach(function () {
|
||||
this.ipc.getUserEditor.resolves({
|
||||
availableEditors: this.availableEditors,
|
||||
})
|
||||
|
||||
cy.get('@button').click()
|
||||
})
|
||||
|
||||
it('opens modal with available editors', function () {
|
||||
this.availableEditors.forEach(({ name }) => {
|
||||
cy.contains(name)
|
||||
})
|
||||
|
||||
cy.contains('Set preference and open file')
|
||||
})
|
||||
|
||||
it('closes modal when cancel is clicked', function () {
|
||||
cy.contains('Cancel').click()
|
||||
cy.contains('Set preference and open file').should('not.be.visible')
|
||||
})
|
||||
|
||||
describe('when editor is not selected', function () {
|
||||
it('disables submit button', function () {
|
||||
cy.contains('Set preference and open file')
|
||||
.should('have.class', 'is-disabled')
|
||||
.click()
|
||||
.then(function () {
|
||||
expect(this.ipc.setUserEditor).not.to.be.called
|
||||
expect(this.ipc.openFile).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('shows validation message when hovering over submit button', function () {
|
||||
cy.get('.editor-picker-modal .submit').trigger('mouseover')
|
||||
cy.get('.cy-tooltip').should('have.text', 'Please select a preference')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Other is selected but path is not entered', function () {
|
||||
beforeEach(function () {
|
||||
cy.contains('Other').click()
|
||||
})
|
||||
|
||||
it('disables submit button', function () {
|
||||
cy.contains('Set preference and open file')
|
||||
.should('have.class', 'is-disabled')
|
||||
.click()
|
||||
.then(function () {
|
||||
expect(this.ipc.setUserEditor).not.to.be.called
|
||||
expect(this.ipc.openFile).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('shows validation message when hovering over submit button', function () {
|
||||
cy.get('.editor-picker-modal .submit').trigger('mouseover')
|
||||
cy.get('.cy-tooltip').should('have.text', 'Please enter the path for the "Other" editor')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when editor is set', function () {
|
||||
beforeEach(function () {
|
||||
cy.contains('Visual Studio Code').click()
|
||||
cy.contains('Set preference and open file').click()
|
||||
})
|
||||
|
||||
it('closes modal', function () {
|
||||
cy.contains('Set preference and open file').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('sets user editor', function () {
|
||||
expect(this.ipc.setUserEditor).to.be.calledWith(this.availableEditors[4])
|
||||
})
|
||||
|
||||
it('opens file in selected editor', function () {
|
||||
expect(this.ipc.openFile).to.be.calledWith({
|
||||
where: this.availableEditors[4],
|
||||
file: '/user/project/cypress/integration/app_spec.coffee',
|
||||
line: 0,
|
||||
column: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,6 +50,7 @@ register('launch:browser', false)
|
||||
register('log:out')
|
||||
register('on:focus:tests', false)
|
||||
register('on:menu:clicked', false)
|
||||
register('open:file')
|
||||
register('open:finder')
|
||||
register('open:project', false)
|
||||
register('on:config:changed', false)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'styles/mixins';
|
||||
@import 'styles/vendor';
|
||||
@import 'styles/components/*';
|
||||
@import '../../ui-components/src/file-opener/file-opener';
|
||||
@import '!(styles)*/**/*';
|
||||
@import '../../../node_modules/@reach/dialog/styles.css';
|
||||
@import '../../ui-components/src/dropdown';
|
||||
@import '../../ui-components/src/editor-picker';
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { observer } from 'mobx-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import ipc from '../lib/ipc'
|
||||
|
||||
import { FileOpener as Opener } from '@packages/ui-components'
|
||||
|
||||
const openFile = (where, { absoluteFile: file, line, column }) => {
|
||||
ipc.openFile({
|
||||
where,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
})
|
||||
}
|
||||
|
||||
const getUserEditor = (callback) => {
|
||||
ipc.getUserEditor().then(callback)
|
||||
}
|
||||
|
||||
const FileOpener = observer((props) => {
|
||||
const fileDetails = {
|
||||
column: 0,
|
||||
line: 0,
|
||||
...props.fileDetails,
|
||||
}
|
||||
|
||||
return (
|
||||
<Opener
|
||||
fileDetails={fileDetails}
|
||||
openFile={openFile}
|
||||
getUserEditor={getUserEditor}
|
||||
setUserEditor={ipc.setUserEditor}
|
||||
className={props.className}
|
||||
>
|
||||
<span><i className="fas fa-external-link-alt fa-sm" /> Open in IDE</span>
|
||||
</Opener>
|
||||
)
|
||||
})
|
||||
|
||||
const fileDetails = PropTypes.shape({
|
||||
absoluteFile: PropTypes.string.isRequired,
|
||||
originalFile: PropTypes.string.isRequired,
|
||||
relativeFile: PropTypes.string.isRequired,
|
||||
})
|
||||
|
||||
FileOpener.propTypes = {
|
||||
fileDetails: fileDetails.isRequired,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
export default FileOpener
|
||||
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react'
|
||||
import Loader from 'react-loader'
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import FileOpener from './file-opener'
|
||||
import ipc from '../lib/ipc'
|
||||
import projectsApi from '../projects/projects-api'
|
||||
import specsStore, { allSpecsSpec } from './specs-store'
|
||||
@@ -28,7 +29,7 @@ class SpecsList extends Component {
|
||||
'show-clear-filter': !!specsStore.filter,
|
||||
})}>
|
||||
<label htmlFor='filter'>
|
||||
<i className='fas fa-search'></i>
|
||||
<i className='fas fa-search' />
|
||||
</label>
|
||||
<input
|
||||
id='filter'
|
||||
@@ -49,7 +50,7 @@ class SpecsList extends Component {
|
||||
<a onClick={this._selectSpec.bind(this, allSpecsSpec)}
|
||||
title="Run all integration specs together"
|
||||
className={cs('all-tests', { active: specsStore.isChosen(allSpecsSpec) })}>
|
||||
<i className={`fa-fw ${this._allSpecsIcon(specsStore.isChosen(allSpecsSpec))}`}></i>{' '}
|
||||
<i className={`fa-fw ${this._allSpecsIcon(specsStore.isChosen(allSpecsSpec))}`} />{' '}
|
||||
{allSpecsSpec.displayName}
|
||||
</a>
|
||||
</header>
|
||||
@@ -140,8 +141,8 @@ class SpecsList extends Component {
|
||||
<li key={spec.path} className={`folder level-${nestingLevel} ${isExpanded ? 'folder-expanded' : 'folder-collapsed'}`}>
|
||||
<div>
|
||||
<div className="folder-name" onClick={this._selectSpecFolder.bind(this, spec)}>
|
||||
<i className={`folder-collapse-icon fas fa-fw ${isExpanded ? 'fa-caret-down' : 'fa-caret-right'}`}></i>
|
||||
{nestingLevel !== 0 ? <i className={`far fa-fw ${isExpanded ? 'fa-folder-open' : 'fa-folder'}`}></i> : null}
|
||||
<i className={`folder-collapse-icon fas fa-fw ${isExpanded ? 'fa-caret-down' : 'fa-caret-right'}`} />
|
||||
{nestingLevel !== 0 ? <i className={`far fa-fw ${isExpanded ? 'fa-folder-open' : 'fa-folder'}`} /> : null}
|
||||
{
|
||||
nestingLevel === 0 ?
|
||||
<>
|
||||
@@ -172,19 +173,21 @@ class SpecsList extends Component {
|
||||
}
|
||||
|
||||
_specContent (spec, nestingLevel) {
|
||||
const fileDetails = {
|
||||
absoluteFile: spec.absolute,
|
||||
originalFile: spec.relative,
|
||||
relativeFile: spec.relative,
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={spec.path} className={`file level-${nestingLevel}`}>
|
||||
<a href='#' onClick={this._selectSpec.bind(this, spec)} className={cs({ active: specsStore.isChosen(spec) })}>
|
||||
<div>
|
||||
<div className="file-name">
|
||||
<i className={`fa-fw ${this._specIcon(specsStore.isChosen(spec))}`}></i>
|
||||
{spec.displayName}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div></div>
|
||||
<li key={spec.path} className={cs(`file level-${nestingLevel}`, { active: specsStore.isChosen(spec) })}>
|
||||
<a href='#' onClick={this._selectSpec.bind(this, spec)} className="file-name-wrapper">
|
||||
<div className="file-name">
|
||||
<i className={`fa-fw ${this._specIcon(specsStore.isChosen(spec))}`} />
|
||||
{spec.displayName}
|
||||
</div>
|
||||
</a>
|
||||
<FileOpener fileDetails={fileDetails} className="file-open-in-ide" />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -200,7 +203,7 @@ class SpecsList extends Component {
|
||||
</code>
|
||||
</h5>
|
||||
<a className='helper-docs-link' onClick={this._openHelp}>
|
||||
<i className='fas fa-question-circle'></i>{' '}
|
||||
<i className='fas fa-question-circle' />{' '}
|
||||
Need help?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -169,40 +169,57 @@ $max-nesting-level: 14;
|
||||
}
|
||||
}
|
||||
|
||||
.file > a {
|
||||
font-weight: 400;
|
||||
.file {
|
||||
border-bottom: 1px dotted #eeeeee;
|
||||
padding: 4px 0;
|
||||
font-family: $font-sans;
|
||||
color: #637eb9;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
|
||||
&.active {
|
||||
pointer-events: none;
|
||||
background-color: #F5FBF7;
|
||||
color: #4c4e63;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
pointer-events: none;
|
||||
background-color: #F5FBF7;
|
||||
color: #4c4e63;
|
||||
> .file-name-wrapper {
|
||||
color: #4c4e63 !important;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: #f8f8f8;
|
||||
color: #38589c;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list-as-table>.file, .list-as-table>.file>a {
|
||||
float: left;
|
||||
width: 100%;
|
||||
> .file-open-in-ide {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
> .file-name-wrapper {
|
||||
color: #637eb9;
|
||||
flex-grow: 1;
|
||||
font-family: $font-sans;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
padding: 4px 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: #38589c;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .file-open-in-ide {
|
||||
align-items: center;
|
||||
color: #727474;
|
||||
display: none;
|
||||
font-family: $font-sans;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
padding: 4px 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(186, 186, 186, 0.2);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-as-table>.file {
|
||||
@@ -217,3 +234,13 @@ $max-nesting-level: 14;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overwrite file opener styles
|
||||
[data-reach-dialog-content] {
|
||||
font-size: 1em;
|
||||
padding-top: 1em;
|
||||
|
||||
h1 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import _ from 'lodash'
|
||||
import { computed, observable } from 'mobx'
|
||||
|
||||
import { FileDetails } from '../opener/file-model'
|
||||
import { FileDetails } from '@packages/ui-components'
|
||||
|
||||
interface ParsedStackMessageLine {
|
||||
message: string
|
||||
|
||||
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react'
|
||||
import Prism from 'prismjs'
|
||||
|
||||
import { CodeFrame } from './err-model'
|
||||
import FileOpener from '../opener/file-opener'
|
||||
import FileOpener from '../lib/file-opener'
|
||||
|
||||
interface Props {
|
||||
codeFrame: CodeFrame
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { ReactElement } from 'react'
|
||||
|
||||
import FileOpener from '../opener/file-opener'
|
||||
import FileOpener from '../lib/file-opener'
|
||||
import Err from './err-model'
|
||||
|
||||
const cypressLineRegex = /(cypress:\/\/|cypress_runner\.js)/
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { observer } from 'mobx-react'
|
||||
import React from 'react'
|
||||
// @ts-ignore
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
|
||||
import events from './events'
|
||||
|
||||
import { GetUserEditorResult, Editor, FileDetails, FileOpener as Opener } from '@packages/ui-components'
|
||||
|
||||
interface Props {
|
||||
fileDetails: FileDetails,
|
||||
className?: string
|
||||
}
|
||||
|
||||
const openFile = (where: Editor, { absoluteFile: file, line, column }: FileDetails) => {
|
||||
events.emit('open:file', {
|
||||
where,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
})
|
||||
}
|
||||
|
||||
const getUserEditor = (callback: (result: GetUserEditorResult) => any) => {
|
||||
events.emit('get:user:editor', callback)
|
||||
}
|
||||
|
||||
const setUserEditor = (editor: Editor) => {
|
||||
events.emit('set:user:editor', editor)
|
||||
}
|
||||
|
||||
const FileOpener = observer((props: Props) => {
|
||||
const { originalFile, line, column } = props.fileDetails
|
||||
|
||||
return (
|
||||
<Tooltip title={'Open in IDE'} wrapperClassName={props.className} className='cy-tooltip'>
|
||||
<span>
|
||||
<Opener
|
||||
openFile={openFile}
|
||||
getUserEditor={getUserEditor}
|
||||
setUserEditor={setUserEditor}
|
||||
fileDetails={props.fileDetails}
|
||||
>
|
||||
{originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
|
||||
</Opener>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
export default FileOpener
|
||||
@@ -3,7 +3,6 @@
|
||||
@import 'lib/variables';
|
||||
@import 'lib/base';
|
||||
@import 'lib/tooltip';
|
||||
@import 'lib/modal';
|
||||
@import '../../../node_modules/@reach/dialog/styles.css';
|
||||
@import '../../ui-components/src/editor-picker';
|
||||
@import '../../ui-components/src/file-opener/file-opener';
|
||||
@import './!(lib)*/**/*';
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
@import 'lib/fonts';
|
||||
@import 'lib/base';
|
||||
@import 'lib/tooltip';
|
||||
@import 'lib/modal';
|
||||
@import '../../../node_modules/@reach/dialog/styles.css';
|
||||
@import '../../ui-components/src/editor-picker';
|
||||
@import '../../ui-components/src/file-opener/file-opener';
|
||||
@import '!(lib)*/**/*';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export interface FileDetails {
|
||||
absoluteFile: string
|
||||
column: number
|
||||
line: number
|
||||
originalFile: string
|
||||
relativeFile: string
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
.editor-picker-modal {
|
||||
max-width: 40em;
|
||||
|
||||
.editor-picker {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
> span:first-child {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
button.is-disabled,
|
||||
button.is-disabled:hover,
|
||||
button.is-disabled:focus {
|
||||
background: $pass !important;
|
||||
cursor: default !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
padding: 1em 1em 1em;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component, ReactElement } from 'react'
|
||||
|
||||
import FileOpener from '../opener/file-opener'
|
||||
import FileOpener from '../lib/file-opener'
|
||||
|
||||
const renderRunnableHeader = (children:ReactElement) => <div className="runnable-header">{children}</div>
|
||||
|
||||
|
||||
@@ -254,8 +254,8 @@
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
span > a:before,
|
||||
span > span:before {
|
||||
span > span > a:before,
|
||||
span > span > span:before {
|
||||
@extend .#{$fa-css-prefix};
|
||||
@extend .#{$fa-css-prefix}-file;
|
||||
color: #bdbdbd;
|
||||
|
||||
@@ -21,6 +21,7 @@ const chromePolicyCheck = require('../util/chrome_policy_check')
|
||||
const browsers = require('../browsers')
|
||||
const konfig = require('../konfig')
|
||||
const editors = require('../util/editors')
|
||||
const fileOpener = require('../util/file-opener')
|
||||
|
||||
const nullifyUnserializableValues = (obj) => {
|
||||
// nullify values that cannot be cloned
|
||||
@@ -167,6 +168,9 @@ const handleEvent = function (options, bus, event, id, type, arg) {
|
||||
case 'window:close':
|
||||
return Windows.getByWebContents(event.sender).destroy()
|
||||
|
||||
case 'open:file':
|
||||
return fileOpener.openFile(arg)
|
||||
|
||||
case 'open:finder':
|
||||
return open.opn(arg)
|
||||
.then(send)
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
|
||||
import { FileOpener } from '../../'
|
||||
|
||||
const _ = Cypress._
|
||||
|
||||
const fileDetails = {
|
||||
absoluteFile: '/absolute/path/to/file_spec.js',
|
||||
column: 0,
|
||||
line: 0,
|
||||
originalFile: 'path/to/file_spec.js',
|
||||
relativeFile: 'path/to/file_spec.js',
|
||||
}
|
||||
|
||||
const preferredOpener = {
|
||||
id: 'vscode',
|
||||
name: 'VS Code',
|
||||
openerId: 'vscode',
|
||||
isOther: false,
|
||||
}
|
||||
|
||||
const availableEditors = [
|
||||
{ id: 'computer', name: 'On Computer', openerId: 'computer', isOther: false, description: 'Opens on computer etc etc' },
|
||||
{ id: 'atom', name: 'Atom', openerId: 'atom', isOther: false },
|
||||
{ id: 'sublime', name: 'Sublime Text', openerId: 'sublime', isOther: false },
|
||||
{ id: 'vscode', name: 'VS Code', openerId: 'vscode', isOther: false },
|
||||
{ id: 'other', name: 'Other', openerId: '', isOther: true, description: 'Enter the full path etc etc' },
|
||||
]
|
||||
|
||||
describe('<FileOpener />', () => {
|
||||
let defaultProps
|
||||
|
||||
beforeEach(() => {
|
||||
defaultProps = {
|
||||
fileDetails,
|
||||
openFile: () => {},
|
||||
getUserEditor: (callback) => {
|
||||
callback({
|
||||
preferredOpener,
|
||||
availableEditors,
|
||||
})
|
||||
},
|
||||
setUserEditor: () => {},
|
||||
className: 'file-opener',
|
||||
}
|
||||
|
||||
cy.visit('dist/index.html')
|
||||
cy.viewport(600, 600)
|
||||
})
|
||||
|
||||
it('renders link text', () => {
|
||||
cy.render(render, <FileOpener {...defaultProps}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').should('have.text', 'Open in IDE')
|
||||
})
|
||||
|
||||
context('when user has already set opener and opens file', () => {
|
||||
it('opens in preferred opener', () => {
|
||||
const openFile = cy.stub()
|
||||
|
||||
cy.render(render, <FileOpener {...defaultProps} openFile={openFile}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click().then(() => {
|
||||
expect(openFile).to.be.calledWith(preferredOpener, fileDetails)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('when user has not already set opener and opens file', () => {
|
||||
let defaultPropsModal
|
||||
|
||||
beforeEach(() => {
|
||||
defaultPropsModal = {
|
||||
...defaultProps,
|
||||
getUserEditor: (callback) => {
|
||||
callback({
|
||||
availableEditors,
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('opens modal with available editors', () => {
|
||||
cy.render(render, <FileOpener {...defaultPropsModal}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
|
||||
_.each(availableEditors, ({ name }) => {
|
||||
cy.contains(name)
|
||||
})
|
||||
|
||||
cy.contains('Set preference and open file')
|
||||
})
|
||||
|
||||
it('closes modal when cancel is clicked', () => {
|
||||
cy.render(render, <FileOpener {...defaultPropsModal}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.contains('Sublime Text').click()
|
||||
cy.contains('Cancel').click()
|
||||
cy.contains('Set preference and open file').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('initially has no editors chosen', () => {
|
||||
cy.render(render, <FileOpener {...defaultPropsModal}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.get('input[type="radio"]').should('not.be.checked')
|
||||
cy.get('.submit').should('have.class', 'is-disabled')
|
||||
})
|
||||
|
||||
it('should not open without editor selected', () => {
|
||||
const setEditor = cy.stub()
|
||||
const openFile = cy.stub()
|
||||
|
||||
cy.render(render, <FileOpener {...defaultPropsModal} setEditor={setEditor} openFile={openFile}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.get('.submit')
|
||||
.should('have.class', 'is-disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(setEditor).not.to.be.called
|
||||
expect(openFile).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('disables submit when Other is selected but path not entered', () => {
|
||||
const setEditor = cy.stub()
|
||||
const openFile = cy.stub()
|
||||
|
||||
cy.render(render, <FileOpener {...defaultPropsModal} setEditor={setEditor} openFile={openFile}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.contains('Other').click()
|
||||
cy.get('.submit')
|
||||
.should('have.class', 'is-disabled')
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(setEditor).not.to.be.called
|
||||
expect(openFile).not.to.be.called
|
||||
})
|
||||
})
|
||||
|
||||
it('sets user editor when selected', () => {
|
||||
const setEditor = cy.stub()
|
||||
|
||||
cy.render(render, <FileOpener {...defaultPropsModal} setUserEditor={setEditor}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.contains('Sublime Text').click()
|
||||
cy.get('.submit').click().then(() => {
|
||||
expect(setEditor).to.be.calledWith(availableEditors[2])
|
||||
})
|
||||
})
|
||||
|
||||
it('opens in correct editor when selected', () => {
|
||||
const openFile = cy.stub()
|
||||
|
||||
cy.render(render, <FileOpener {...defaultPropsModal} openFile={openFile}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.contains('Sublime Text').click()
|
||||
cy.get('.submit').click().then(() => {
|
||||
expect(openFile).to.be.calledWith(availableEditors[2], fileDetails)
|
||||
})
|
||||
})
|
||||
|
||||
it('closes modal after selection', () => {
|
||||
cy.render(render, <FileOpener {...defaultPropsModal}>Open in IDE</FileOpener>)
|
||||
|
||||
cy.get('.file-opener').click()
|
||||
cy.contains('Sublime Text').click()
|
||||
cy.get('.submit').click()
|
||||
cy.contains('Set preference and open file').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -2,12 +2,12 @@ const wp = require('@cypress/webpack-preprocessor')
|
||||
const webpackOptions = {
|
||||
mode: 'none',
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.png'],
|
||||
extensions: ['.ts', '.js', '.jsx', '.tsx', '.png'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
test: /\.(ts|js|jsx|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: require.resolve('babel-loader'),
|
||||
@@ -19,6 +19,7 @@ const webpackOptions = {
|
||||
presets: [
|
||||
require.resolve('@babel/preset-env'),
|
||||
require.resolve('@babel/preset-react'),
|
||||
require.resolve('@babel/preset-typescript'),
|
||||
],
|
||||
babelrc: false,
|
||||
},
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
@import "../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss";
|
||||
@import "../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
|
||||
@import '../../../../node_modules/@reach/dialog/styles.css';
|
||||
@import '../../src/dropdown';
|
||||
@import '../../src/editor-picker';
|
||||
@import '../../src/file-opener/file-opener';
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/// <reference path="../../cli/types/cy-blob-util.d.ts" />
|
||||
/// <reference path="../../cli/types/cy-bluebird.d.ts" />
|
||||
/// <reference path="../../cli/types/cy-moment.d.ts" />
|
||||
/// <reference path="../../cli/types/cy-minimatch.d.ts" />
|
||||
|
||||
/// <reference path="../../cli/types/cypress.d.ts" />
|
||||
/// <reference path="../../cli/types/cypress-global-vars.d.ts" />
|
||||
/// <reference path="../../cli/types/cypress-type-helpers.d.ts" />
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@packages/ui-components",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.jsx",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build-for-tests": "webpack",
|
||||
"check-deps": "node ../../scripts/check-deps.js --verbose",
|
||||
@@ -20,6 +20,7 @@
|
||||
"@babel/preset-env": "7.9.0",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@fortawesome/fontawesome-free": "5.12.1",
|
||||
"@reach/dialog": "0.6.1",
|
||||
"@reach/visually-hidden": "0.6.2",
|
||||
"babel-loader": "8.1.0",
|
||||
"browser-logos": "github:alrra/browser-logos",
|
||||
|
||||
@@ -2,5 +2,25 @@
|
||||
"extends": [
|
||||
"plugin:@cypress/dev/react",
|
||||
"plugin:@cypress/dev/tests"
|
||||
]
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "16.12"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"arrow-body-style": "off",
|
||||
"no-unused-vars": "off",
|
||||
"react/jsx-filename-extension": [
|
||||
"warn",
|
||||
{
|
||||
"extensions": [
|
||||
".js",
|
||||
".jsx",
|
||||
".tsx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+22
-8
@@ -1,7 +1,11 @@
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
|
||||
const families = {
|
||||
interface FamilyOptions {
|
||||
[key: string]: RegExp
|
||||
}
|
||||
|
||||
const families: FamilyOptions = {
|
||||
chrome: /^chrome/i,
|
||||
chromium: /^chromium/i,
|
||||
edge: /^edge/i,
|
||||
@@ -9,7 +13,11 @@ const families = {
|
||||
firefox: /^firefox/i,
|
||||
}
|
||||
|
||||
const logoPaths = {
|
||||
interface LogoOptions {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const logoPaths: LogoOptions = {
|
||||
canary: require('browser-logos/src/chrome-canary/chrome-canary_32x32.png'),
|
||||
chrome: require('browser-logos/src/chrome/chrome_32x32.png'),
|
||||
chromium: require('browser-logos/src/chromium/chromium_32x32.png'),
|
||||
@@ -23,24 +31,30 @@ const logoPaths = {
|
||||
firefoxNightly: require('browser-logos/src/firefox-nightly/firefox-nightly_32x32.png'),
|
||||
}
|
||||
|
||||
const familyFallback = (browserKey) => {
|
||||
const familyFallback = (browserKey: string) => {
|
||||
return _.reduce(families, (found, regex, family) => {
|
||||
if (found) return found
|
||||
if (found !== '') return found
|
||||
|
||||
if (regex.test(browserKey)) return family
|
||||
}, null)
|
||||
|
||||
return ''
|
||||
}, '')
|
||||
}
|
||||
|
||||
const logoPath = (browserName) => {
|
||||
const logoPath = (browserName: string) => {
|
||||
const browserKey = _.camelCase(browserName)
|
||||
|
||||
return logoPaths[browserKey] || logoPaths[familyFallback(browserKey)]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
browserName: string
|
||||
}
|
||||
|
||||
// browserName should be the browser's display name
|
||||
const BrowserIcon = ({ browserName }) => {
|
||||
const BrowserIcon = ({ browserName }: Props) => {
|
||||
if (logoPath(browserName)) {
|
||||
return <img className='browser-icon' src={logoPath(browserName)} />
|
||||
return <img className='browser-icon' src={logoPath(browserName)} alt={browserName} />
|
||||
}
|
||||
|
||||
return <i className='browser-icon fas fa-fw fa-globe' />
|
||||
@@ -1,31 +1,36 @@
|
||||
import cs from 'classnames'
|
||||
import _ from 'lodash'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import React, { Component, ReactNode } from 'react'
|
||||
import { findDOMNode } from 'react-dom'
|
||||
|
||||
class Dropdown extends Component {
|
||||
interface Indexable {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
chosen: Indexable
|
||||
others: Indexable[]
|
||||
onSelect: (item: Indexable) => any
|
||||
renderItem: (item: Indexable) => ReactNode
|
||||
keyProperty: string
|
||||
disabled?: boolean
|
||||
document: Document
|
||||
}
|
||||
|
||||
class Dropdown extends Component<Props> {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
document,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
chosen: PropTypes.object.isRequired,
|
||||
others: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
// property for unique value on each item that can be used as its key
|
||||
keyProperty: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = { open: false }
|
||||
|
||||
outsideClickHandler: (e: Event) => void = () => {}
|
||||
|
||||
componentDidMount () {
|
||||
this.outsideClickHandler = (e) => {
|
||||
if (!findDOMNode(this).contains(e.target)) {
|
||||
this.outsideClickHandler = (e: Event) => {
|
||||
if (!findDOMNode(this)?.contains(e.target as Node)) {
|
||||
this.setState({ open: false })
|
||||
}
|
||||
}
|
||||
@@ -76,7 +81,7 @@ class Dropdown extends Component {
|
||||
|
||||
return (
|
||||
<span className='dropdown-toggle'>
|
||||
<span className='dropdown-caret'></span>
|
||||
<span className='dropdown-caret' />
|
||||
<span className='sr-only'>Toggle Dropdown</span>
|
||||
</span>
|
||||
)
|
||||
@@ -94,7 +99,7 @@ class Dropdown extends Component {
|
||||
{_.map(this.props.others, (item) => (
|
||||
<li
|
||||
key={item[this.props.keyProperty]}
|
||||
tabIndex='0'
|
||||
tabIndex={0}
|
||||
onClick={() => this._onSelect(item)}
|
||||
>{this.props.renderItem(item)}</li>
|
||||
))}
|
||||
@@ -102,7 +107,7 @@ class Dropdown extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
_onSelect (item) {
|
||||
_onSelect (item: object) {
|
||||
this.setState({ open: false })
|
||||
this.props.onSelect(item)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import _ from 'lodash'
|
||||
import PropTypes from 'prop-types'
|
||||
import { observer, PropTypes as MobxPropTypes } from 'mobx-react'
|
||||
import React from 'react'
|
||||
|
||||
import { Select, SelectItem } from './select'
|
||||
|
||||
const EditorPicker = observer(({ chosen = {}, editors, onSelect, onUpdateOtherPath }) => {
|
||||
const editorOptions = _.reject(editors, { isOther: true })
|
||||
const otherOption = _.find(editors, { isOther: true })
|
||||
|
||||
const onChange = (id) => {
|
||||
const editor = _.find(editors, { id })
|
||||
|
||||
onSelect(editor)
|
||||
}
|
||||
|
||||
const updateOtherPath = (event) => {
|
||||
onUpdateOtherPath(_.trim(event.target.value || ''))
|
||||
}
|
||||
|
||||
const otherInput = (
|
||||
<input
|
||||
type='text'
|
||||
className='other-input'
|
||||
value={otherOption.openerId || ''}
|
||||
onFocus={_.partial(onChange, 'other')}
|
||||
onChange={updateOtherPath}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select value={chosen.id} className='editor-picker' name='editor-picker' onChange={onChange}>
|
||||
{_.map(editorOptions, (editor) => (
|
||||
<SelectItem key={editor.id} value={editor.id}>
|
||||
{editor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value={otherOption.id}>
|
||||
{otherOption.name}: {otherInput}
|
||||
{chosen.isOther && <span className='description'>Enter the full path to your editor's executable</span>}
|
||||
</SelectItem>
|
||||
</Select>
|
||||
)
|
||||
})
|
||||
|
||||
const editorType = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
openerId: PropTypes.string.isRequired,
|
||||
isOther: PropTypes.bool.isRequired,
|
||||
description: PropTypes.string,
|
||||
})
|
||||
|
||||
EditorPicker.propTypes = {
|
||||
chosenEditor: editorType,
|
||||
editors: MobxPropTypes.observableArrayOf(editorType).isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onUpdateOtherPath: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default EditorPicker
|
||||
@@ -1,36 +0,0 @@
|
||||
@import './select/select';
|
||||
|
||||
.editor-picker {
|
||||
label {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
border: solid 1px #7e7e7e;
|
||||
border-radius: 3px;
|
||||
margin-left: 0.3em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
.is-selected .other-input {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #7e7e7e;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
i.description {
|
||||
margin-left: 0.4em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
span.description {
|
||||
display: block;
|
||||
padding-left: 5.2em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -8,15 +8,9 @@ import Tooltip from '@cypress/react-tooltip'
|
||||
import cs from 'classnames'
|
||||
import React from 'react'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
// @ts-ignore
|
||||
import { EditorPicker } from '@packages/ui-components'
|
||||
|
||||
export interface Editor {
|
||||
id: string
|
||||
name: string
|
||||
openerId: string
|
||||
isOther: boolean
|
||||
}
|
||||
import EditorPicker from './editor-picker'
|
||||
import { Editor } from './file-model'
|
||||
|
||||
interface Props {
|
||||
chosenEditor: Editor
|
||||
@@ -0,0 +1,56 @@
|
||||
import _ from 'lodash'
|
||||
import { observer } from 'mobx-react'
|
||||
import React, { ChangeEvent } from 'react'
|
||||
|
||||
import { Editor } from './file-model'
|
||||
import { Select, SelectItem } from '../select'
|
||||
|
||||
interface Props {
|
||||
chosen?: Partial<Editor>
|
||||
editors: Editor[]
|
||||
onSelect: (editor: Editor) => any
|
||||
onUpdateOtherPath: (path: string) => any
|
||||
}
|
||||
|
||||
const EditorPicker = observer(({ chosen = {}, editors, onSelect, onUpdateOtherPath }: Props) => {
|
||||
const editorOptions = _.reject(editors, { isOther: true })
|
||||
const otherOption = _.find(editors, { isOther: true })
|
||||
|
||||
const onChange = (id: string) => {
|
||||
const editor = _.find(editors, { id })
|
||||
|
||||
editor && onSelect(editor)
|
||||
}
|
||||
|
||||
const updateOtherPath = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onUpdateOtherPath(_.trim(event.target.value || ''))
|
||||
}
|
||||
|
||||
const otherInput = (
|
||||
<input
|
||||
type='text'
|
||||
className='other-input'
|
||||
value={otherOption?.openerId || ''}
|
||||
onFocus={_.partial(onChange, 'other')}
|
||||
onChange={updateOtherPath}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select value={chosen.id || ''} className='editor-picker' name='editor-picker' onChange={onChange}>
|
||||
{_.map(editorOptions, (editor) => (
|
||||
<SelectItem key={editor.id} value={editor.id}>
|
||||
{editor.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{otherOption && (
|
||||
<SelectItem value={otherOption.id}>
|
||||
{otherOption.name}: {otherInput}
|
||||
{chosen.isOther && <span className='description'>Enter the full path to your editor's executable</span>}
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
})
|
||||
|
||||
export default EditorPicker
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface FileDetails {
|
||||
absoluteFile: string
|
||||
column: number
|
||||
line: number
|
||||
originalFile: string
|
||||
relativeFile: string
|
||||
}
|
||||
|
||||
export interface Editor {
|
||||
id: string
|
||||
name: string
|
||||
openerId: string
|
||||
isOther: boolean
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface GetUserEditorResult {
|
||||
preferredOpener?: Editor
|
||||
availableEditors?: Editor[]
|
||||
}
|
||||
+72
-1
@@ -1,5 +1,70 @@
|
||||
@import '../select/select';
|
||||
|
||||
$pass: #08c18d !default;
|
||||
$font-sans: "Helvetica Neue", Helvetica, Arial, sans-serif !default;
|
||||
|
||||
.editor-picker {
|
||||
label {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
border: solid 1px #7e7e7e;
|
||||
border-radius: 3px;
|
||||
margin-left: 0.3em;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
|
||||
.is-selected .other-input {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #7e7e7e;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
i.description {
|
||||
margin-left: 0.4em;
|
||||
margin-top: 0.1em;
|
||||
}
|
||||
|
||||
span.description {
|
||||
display: block;
|
||||
padding-left: 5.2em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-picker-modal {
|
||||
max-width: 40em;
|
||||
|
||||
.editor-picker {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.controls {
|
||||
> span:first-child {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
button.is-disabled,
|
||||
button.is-disabled:hover,
|
||||
button.is-disabled:focus {
|
||||
background: $pass !important;
|
||||
cursor: default !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
padding: 1em 1em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
[data-reach-dialog-overlay] {
|
||||
display: flex;
|
||||
padding: 2em 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -7,7 +72,6 @@
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-radius: 10px;
|
||||
font-family: $open-sans;
|
||||
font-size: 0.9em;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
@@ -15,6 +79,12 @@
|
||||
padding: 2em 0 0;
|
||||
position: relative;
|
||||
|
||||
@if variable-exists(open-sans) {
|
||||
font-family: $open-sans;
|
||||
} @else {
|
||||
font-family: $font-sans;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.5em;
|
||||
@@ -76,3 +146,4 @@
|
||||
top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-39
@@ -1,34 +1,21 @@
|
||||
import _ from 'lodash'
|
||||
import { action } from 'mobx'
|
||||
import { observer, useLocalStore } from 'mobx-react'
|
||||
import React, { MouseEvent } from 'react'
|
||||
// @ts-ignore
|
||||
import Tooltip from '@cypress/react-tooltip'
|
||||
import React, { MouseEvent, ReactNode } from 'react'
|
||||
|
||||
import EditorPickerModal, { Editor } from './editor-picker-modal'
|
||||
import { FileDetails } from './file-model'
|
||||
import events from '../lib/events'
|
||||
|
||||
interface GetUserEditorResult {
|
||||
preferredOpener?: Editor
|
||||
availableEditors?: Editor[]
|
||||
}
|
||||
import EditorPickerModal from './editor-picker-modal'
|
||||
import { GetUserEditorResult, Editor, FileDetails } from './file-model'
|
||||
|
||||
interface Props {
|
||||
fileDetails: FileDetails,
|
||||
children: ReactNode
|
||||
fileDetails: FileDetails
|
||||
openFile: (where: Editor, absoluteFile: FileDetails) => any
|
||||
getUserEditor: (callback: (result: GetUserEditorResult) => void) => any
|
||||
setUserEditor: (editor: Editor) => any
|
||||
className?: string
|
||||
}
|
||||
|
||||
const openFile = (where: Editor, { absoluteFile: file, line, column }: FileDetails) => {
|
||||
events.emit('open:file', {
|
||||
where,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
})
|
||||
}
|
||||
|
||||
const FileOpener = observer(({ fileDetails, className }: Props) => {
|
||||
const FileOpener = observer(({ children, fileDetails, openFile, getUserEditor, setUserEditor, className }: Props) => {
|
||||
const state = useLocalStore(() => ({
|
||||
editors: [] as Editor[],
|
||||
chosenEditor: {} as Editor,
|
||||
@@ -57,7 +44,7 @@ const FileOpener = observer(({ fileDetails, className }: Props) => {
|
||||
|
||||
// TODO: instead of the back-n-forth, send 'open:file' or similar, and if the
|
||||
// user editor isn't set, it should send back the available editors
|
||||
events.emit('get:user:editor', (result: GetUserEditorResult) => {
|
||||
getUserEditor((result: GetUserEditorResult) => {
|
||||
state.setIsLoadingEditor(false)
|
||||
|
||||
if (result.preferredOpener) {
|
||||
@@ -70,28 +57,24 @@ const FileOpener = observer(({ fileDetails, className }: Props) => {
|
||||
}
|
||||
|
||||
const setEditor = (editor: Editor) => {
|
||||
events.emit('set:user:editor', editor)
|
||||
setUserEditor(editor)
|
||||
state.setIsModalOpen(false)
|
||||
state.setChosenEditor({} as Editor)
|
||||
openFile(editor, fileDetails)
|
||||
}
|
||||
|
||||
const { originalFile, line, column } = fileDetails
|
||||
|
||||
return (
|
||||
<Tooltip title={'Open in IDE'} wrapperClassName={className} className='cy-tooltip'>
|
||||
<a onClick={attemptOpenFile} href='#'>
|
||||
{originalFile}{!!line && `:${line}`}{!!column && `:${column}`}
|
||||
<EditorPickerModal
|
||||
chosenEditor={state.chosenEditor}
|
||||
editors={state.editors}
|
||||
isOpen={state.isModalOpen}
|
||||
onSetEditor={setEditor}
|
||||
onSetChosenEditor={state.setChosenEditor}
|
||||
onClose={_.partial(state.setIsModalOpen, false)}
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
<a className={className} onClick={attemptOpenFile} href='#'>
|
||||
{children}
|
||||
<EditorPickerModal
|
||||
chosenEditor={state.chosenEditor}
|
||||
editors={state.editors}
|
||||
isOpen={state.isModalOpen}
|
||||
onSetEditor={setEditor}
|
||||
onSetChosenEditor={state.setChosenEditor}
|
||||
onClose={_.partial(state.setIsModalOpen, false)}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export { default as BrowserIcon } from './browser-icon'
|
||||
|
||||
export { default as Dropdown } from './dropdown'
|
||||
|
||||
export { default as EditorPicker } from './editor-picker'
|
||||
|
||||
export * from './select'
|
||||
@@ -0,0 +1,13 @@
|
||||
export { default as BrowserIcon } from './browser-icon'
|
||||
|
||||
export { default as Dropdown } from './dropdown'
|
||||
|
||||
export { default as EditorPicker } from './file-opener/editor-picker'
|
||||
|
||||
export { default as EditorPickerModal } from './file-opener/editor-picker-modal'
|
||||
|
||||
export { default as FileOpener } from './file-opener/file-opener'
|
||||
|
||||
export * from './file-opener/file-model'
|
||||
|
||||
export * from './select'
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
export default createContext()
|
||||
@@ -0,0 +1,16 @@
|
||||
import _ from 'lodash'
|
||||
import { createContext, KeyboardEvent } from 'react'
|
||||
|
||||
interface ContextValue {
|
||||
handleChange: (value: string) => any
|
||||
handleKeyDown: (event: KeyboardEvent) => any
|
||||
isSelected: (value: string) => boolean
|
||||
name: string
|
||||
}
|
||||
|
||||
export default createContext<ContextValue>({
|
||||
handleChange: _.noop,
|
||||
handleKeyDown: _.noop,
|
||||
isSelected: () => false,
|
||||
name: '',
|
||||
})
|
||||
+15
-11
@@ -1,9 +1,9 @@
|
||||
import cs from 'classnames'
|
||||
import React, { Children, useCallback, useMemo } from 'react'
|
||||
import React, { Children, KeyboardEvent, ReactElement, ReactNode, useCallback, useMemo } from 'react'
|
||||
import _ from 'lodash'
|
||||
import Context from './context'
|
||||
|
||||
const generateGroupName = (name) => {
|
||||
const generateGroupName = (name?: string) => {
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
@@ -11,12 +11,12 @@ const generateGroupName = (name) => {
|
||||
return _.uniqueId('Select-')
|
||||
}
|
||||
|
||||
const toValues = (children) => {
|
||||
const withSelectItem = _.filter(children, (child) => {
|
||||
const toValues = (children: ReactNode[]) => {
|
||||
const withSelectItem = _.filter(children, (child: ReactElement) => {
|
||||
return child.props.selectItem
|
||||
})
|
||||
|
||||
return _.map(withSelectItem, (child) => child.props.value)
|
||||
return _.map<any, string>(withSelectItem, (child: ReactElement) => child.props.value)
|
||||
}
|
||||
|
||||
const left = 13
|
||||
@@ -24,10 +24,18 @@ const up = 38
|
||||
const right = 39
|
||||
const down = 40
|
||||
|
||||
const Select = ({ children, className, name, onChange, value }) => {
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
name?: string
|
||||
onChange?: (value: string) => any
|
||||
value: string
|
||||
}
|
||||
|
||||
const Select = ({ children, className, name, onChange = _.noop, value }: Props) => {
|
||||
const allValues = useMemo(() => toValues(Children.toArray(children)), [children])
|
||||
|
||||
const handleKeyDown = useCallback(({ keyCode }) => {
|
||||
const handleKeyDown = useCallback(({ keyCode }: KeyboardEvent) => {
|
||||
if (![left, up, right, down].includes(keyCode)) {
|
||||
return
|
||||
}
|
||||
@@ -67,8 +75,4 @@ const Select = ({ children, className, name, onChange, value }) => {
|
||||
)
|
||||
}
|
||||
|
||||
Select.defaultProps = {
|
||||
onChange: _.noop,
|
||||
}
|
||||
|
||||
export default Select
|
||||
+14
-5
@@ -1,17 +1,23 @@
|
||||
import cs from 'classnames'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { KeyboardEvent, ReactNode, useRef } from 'react'
|
||||
import { partial, uniqueId } from 'lodash'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
|
||||
import useSelect from './use-select'
|
||||
|
||||
const SelectItem = ({ value, children, selectItem, ...rest }) => {
|
||||
interface Props {
|
||||
value: string
|
||||
children?: ReactNode
|
||||
selectItem?: boolean
|
||||
}
|
||||
|
||||
const SelectItem = ({ value, children, selectItem, ...rest }: Props) => {
|
||||
const { name, handleChange, handleKeyDown, isSelected } = useSelect()
|
||||
const liRef = useRef()
|
||||
const inputRef = useRef()
|
||||
const liRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
const id = uniqueId('select-item-')
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
// ensure it's not an element in children that's being keyed down
|
||||
if (e.target && e.target !== liRef.current && e.target !== inputRef.current) {
|
||||
return
|
||||
@@ -40,6 +46,9 @@ const SelectItem = ({ value, children, selectItem, ...rest }) => {
|
||||
name={name}
|
||||
type='radio'
|
||||
value={value}
|
||||
// style required to prevent an error from being thrown
|
||||
// when used inside of a dialog (ex. editor picker modal)
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
{children}
|
||||
@@ -15,7 +15,6 @@ const config: webpack.Configuration = {
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
config.plugins = [
|
||||
// @ts-ignore
|
||||
...config.plugins,
|
||||
|
||||
Reference in New Issue
Block a user