mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-02 13:00:18 -05:00
feat: Add notification in Desktop GUI when there is a new version available (#8803)
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"nodeVersion": "system",
|
||||
"testFiles": "**/*_spec.{js,jsx}",
|
||||
"experimentalComponentTesting": true,
|
||||
"experimentalNetworkStubbing": true,
|
||||
"componentFolder": "src",
|
||||
"reporter": "../../node_modules/cypress-multi-reporters/index.js",
|
||||
"reporterOptions": {
|
||||
|
||||
@@ -303,7 +303,7 @@ describe('Login', function () {
|
||||
|
||||
describe('api help link', () => {
|
||||
it('goes to external api help link', () => {
|
||||
cy.contains('Learn more').click().then(function () {
|
||||
cy.get('.login').contains('Learn more').click().then(function () {
|
||||
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/help-connect-to-api')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import human from 'human-interval'
|
||||
import { deferred } from '../support/util'
|
||||
|
||||
const OLD_VERSION = '1.3.3'
|
||||
const NEW_VERSION = '1.3.4'
|
||||
|
||||
describe('Update Modal', () => {
|
||||
let user
|
||||
let start
|
||||
let ipc
|
||||
let updaterCheck
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(800, 500)
|
||||
cy.fixture('user').then((theUser) => user = theUser)
|
||||
cy.fixture('projects').as('projects')
|
||||
cy.fixture('config').as('config')
|
||||
|
||||
cy.visitIndex({
|
||||
onBeforeLoad (win) {
|
||||
cy.spy(win, 'setInterval')
|
||||
},
|
||||
}).then((win) => {
|
||||
start = win.App.start
|
||||
ipc = win.App.ipc
|
||||
|
||||
cy.stub(ipc, 'getCurrentUser').resolves(user)
|
||||
cy.stub(ipc, 'externalOpen')
|
||||
cy.stub(ipc, 'setClipboardText')
|
||||
|
||||
updaterCheck = deferred()
|
||||
|
||||
cy.stub(ipc, 'updaterCheck').returns(updaterCheck.promise)
|
||||
})
|
||||
})
|
||||
|
||||
describe('general behavior', () => {
|
||||
beforeEach(() => {
|
||||
cy.stub(ipc, 'getOptions').resolves({ version: OLD_VERSION })
|
||||
|
||||
start()
|
||||
})
|
||||
|
||||
it('checks for updates every 60 minutes', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.setInterval.firstCall.args[1]).to.eq(human('60 minutes'))
|
||||
})
|
||||
})
|
||||
|
||||
it('checks for update on show', () => {
|
||||
cy.wrap(ipc.updaterCheck).should('be.called')
|
||||
})
|
||||
|
||||
it('gracefully handles error', () => {
|
||||
updaterCheck.reject({ name: 'foo', message: 'Something bad happened' })
|
||||
|
||||
cy.get('.footer').should('be.visible')
|
||||
})
|
||||
|
||||
it('opens modal on click of Update link', () => {
|
||||
updaterCheck.resolve(NEW_VERSION)
|
||||
cy.get('.footer .version').click()
|
||||
|
||||
cy.get('.modal').should('be.visible')
|
||||
})
|
||||
|
||||
it('closes modal when X is clicked', () => {
|
||||
updaterCheck.resolve(NEW_VERSION)
|
||||
cy.get('.footer .version').click()
|
||||
cy.get('.modal').find('.close').click()
|
||||
|
||||
cy.get('.modal').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('in global mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.stub(ipc, 'getOptions').resolves({ version: OLD_VERSION, os: 'linux' })
|
||||
start()
|
||||
updaterCheck.resolve(NEW_VERSION)
|
||||
|
||||
cy.get('.footer .version').click()
|
||||
})
|
||||
|
||||
it('modal has info about downloading new version', () => {
|
||||
cy.get('.modal').contains('Download the new version')
|
||||
})
|
||||
|
||||
it('opens download link when Download is clicked', () => {
|
||||
cy.contains('Download the new version').click().then(() => {
|
||||
expect(ipc.externalOpen).to.be.calledWith('https://download.cypress.io/desktop')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in project mode', () => {
|
||||
const npmCommand = `npm install --save-dev cypress@${NEW_VERSION}`
|
||||
const yarnCommand = `yarn upgrade cypress@${NEW_VERSION}`
|
||||
|
||||
beforeEach(() => {
|
||||
cy.stub(ipc, 'getOptions').resolves({ version: OLD_VERSION, projectRoot: '/foo/bar' })
|
||||
start()
|
||||
updaterCheck.resolve(NEW_VERSION)
|
||||
|
||||
cy.get('.footer .version').click()
|
||||
})
|
||||
|
||||
it('modal has info about upgrading via package manager', () => {
|
||||
cy.get('.modal').contains(npmCommand)
|
||||
cy.get('.modal').contains(yarnCommand)
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('copies npm upgrade command to clipboard', () => {
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
.then(() => {
|
||||
expect(ipc.setClipboardText).to.be.calledWith(npmCommand)
|
||||
})
|
||||
})
|
||||
|
||||
it('changes npm upgrade button icon after copying', () => {
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
cy.contains(npmCommand).find('button i').should('have.class', 'fa-check')
|
||||
})
|
||||
|
||||
it('disables npm upgrade button after copying', () => {
|
||||
cy.contains(npmCommand).find('button').click().should('be.disabled')
|
||||
})
|
||||
|
||||
it('resets npm upgrade button after 5 seconds', () => {
|
||||
cy.clock()
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
cy.tick(5000)
|
||||
cy.contains(npmCommand).find('button i').should('have.class', 'fa-copy')
|
||||
cy.contains(npmCommand).find('button').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('copies yarn upgrade command to clipboard', () => {
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
.then(() => {
|
||||
expect(ipc.setClipboardText).to.be.calledWith(yarnCommand)
|
||||
})
|
||||
})
|
||||
|
||||
it('changes yarn upgrade button icon after copying', () => {
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
cy.contains(yarnCommand).find('button i').should('have.class', 'fa-check')
|
||||
})
|
||||
|
||||
it('disables yarn upgrade button after copying', () => {
|
||||
cy.contains(yarnCommand).find('button').click().should('be.disabled')
|
||||
})
|
||||
|
||||
it('resets yarn upgrade button after 5 seconds', () => {
|
||||
cy.clock()
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
cy.tick(5000)
|
||||
cy.contains(yarnCommand).find('button i').should('have.class', 'fa-copy')
|
||||
cy.contains(yarnCommand).find('button').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('links to \'open\' doc on click of open command', () => {
|
||||
cy.contains('cypress open').click().then(() => {
|
||||
expect(ipc.externalOpen).to.be.calledWith('https://on.cypress.io/how-to-open-cypress')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,90 @@
|
||||
import human from 'human-interval'
|
||||
import { deferred } from '../support/util'
|
||||
|
||||
describe('Update Notice', () => {
|
||||
let user
|
||||
let ipc
|
||||
let start
|
||||
let updaterCheck
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(800, 500)
|
||||
cy.fixture('user').then((theUser) => user = theUser)
|
||||
cy.fixture('projects').as('projects')
|
||||
cy.fixture('config').as('config')
|
||||
|
||||
cy.visitIndex().then((win) => {
|
||||
ipc = win.App.ipc
|
||||
start = win.App.start
|
||||
|
||||
cy.stub(ipc, 'getCurrentUser').resolves(user)
|
||||
cy.stub(ipc, 'getOptions').resolves({ version: '1.0.0' })
|
||||
|
||||
updaterCheck = deferred()
|
||||
|
||||
cy.stub(ipc, 'updaterCheck').returns(updaterCheck.promise)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not appear if up-to-date', () => {
|
||||
start()
|
||||
cy.wait(500) // need to wait for animation or it will falsely appear invisible
|
||||
cy.get('.update-notice').should('not.be.visible')
|
||||
})
|
||||
|
||||
describe('when there is an update', () => {
|
||||
beforeEach(() => {
|
||||
start()
|
||||
updaterCheck.resolve('1.0.1')
|
||||
})
|
||||
|
||||
it('shows update notice', () => {
|
||||
cy.get('.update-notice').should('be.visible')
|
||||
cy.get('.update-notice .content').should('have.text', 'An update (1.0.1) is available. Learn more')
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('clicking on "Learn more" opens update modal and closes notice', () => {
|
||||
cy.get('.update-notice').contains('Learn more').click()
|
||||
|
||||
cy.get('.update-modal').should('be.visible')
|
||||
cy.get('.update-notice').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('clicking close button closes notice', () => {
|
||||
cy.get('.update-notice .notification-close').click()
|
||||
|
||||
cy.get('.update-notice').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an update that has already been dismissed', () => {
|
||||
let updaterCheck2
|
||||
|
||||
beforeEach(() => {
|
||||
cy.clock()
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('dismissed-update-version', JSON.stringify('1.0.1'))
|
||||
updaterCheck2 = deferred()
|
||||
ipc.updaterCheck.onCall(1).returns(updaterCheck2.promise)
|
||||
|
||||
start()
|
||||
updaterCheck.resolve('1.0.1')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show update notice', () => {
|
||||
cy.wait(500) // need to wait for animation or it will falsely appear invisible
|
||||
cy.get('.update-notice').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('shows update notice when a newer version is available', () => {
|
||||
cy.tick(human('60 minutes')).then(() => {
|
||||
updaterCheck2.resolve('1.0.2')
|
||||
})
|
||||
|
||||
cy.get('.update-notice').should('be.visible')
|
||||
cy.get('.update-notice .content').should('have.text', 'An update (1.0.2) is available. Learn more')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,164 +0,0 @@
|
||||
const human = require('human-interval')
|
||||
|
||||
const OLD_VERSION = '1.3.3'
|
||||
const NEW_VERSION = '1.3.4'
|
||||
|
||||
describe('Updates', function () {
|
||||
beforeEach(function () {
|
||||
cy.viewport(800, 500)
|
||||
cy.fixture('user').as('user')
|
||||
cy.fixture('projects').as('projects')
|
||||
cy.fixture('projects_statuses').as('projectStatuses')
|
||||
cy.fixture('config').as('config')
|
||||
cy.fixture('specs').as('specs')
|
||||
|
||||
cy.visitIndex({
|
||||
onBeforeLoad (win) {
|
||||
cy.spy(win, 'setInterval')
|
||||
},
|
||||
}).then(function (win) {
|
||||
({ start: this.start, ipc: this.ipc } = win.App)
|
||||
|
||||
cy.stub(this.ipc, 'getCurrentUser').resolves(this.user)
|
||||
cy.stub(this.ipc, 'windowOpen')
|
||||
cy.stub(this.ipc, 'externalOpen')
|
||||
cy.stub(this.ipc, 'setClipboardText')
|
||||
|
||||
this.updaterCheck = this.util.deferred()
|
||||
|
||||
cy.stub(this.ipc, 'updaterCheck').returns(this.updaterCheck.promise)
|
||||
})
|
||||
})
|
||||
|
||||
describe('general behavior', function () {
|
||||
beforeEach(function () {
|
||||
cy.stub(this.ipc, 'getOptions').resolves({ version: OLD_VERSION })
|
||||
|
||||
this.start()
|
||||
})
|
||||
|
||||
it('checks for updates every 60 minutes', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.setInterval.firstCall.args[1]).to.eq(human('60 minutes'))
|
||||
})
|
||||
})
|
||||
|
||||
it('checks for update on show', function () {
|
||||
cy.wrap(this.ipc.updaterCheck).should('be.called')
|
||||
})
|
||||
|
||||
it('gracefully handles error', function () {
|
||||
this.updaterCheck.reject({ name: 'foo', message: 'Something bad happened' })
|
||||
|
||||
cy.get('.footer').should('be.visible')
|
||||
})
|
||||
|
||||
it('opens modal on click of Update link', function () {
|
||||
this.updaterCheck.resolve(NEW_VERSION)
|
||||
cy.get('.footer .version').click()
|
||||
|
||||
cy.get('.modal').should('be.visible')
|
||||
})
|
||||
|
||||
it('closes modal when X is clicked', function () {
|
||||
this.updaterCheck.resolve(NEW_VERSION)
|
||||
cy.get('.footer .version').click()
|
||||
cy.get('.modal').find('.close').click()
|
||||
|
||||
cy.get('.modal').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('in global mode', function () {
|
||||
beforeEach(function () {
|
||||
cy.stub(this.ipc, 'getOptions').resolves({ version: OLD_VERSION, os: 'linux' })
|
||||
this.start()
|
||||
this.updaterCheck.resolve(NEW_VERSION)
|
||||
|
||||
cy.get('.footer .version').click()
|
||||
})
|
||||
|
||||
it('modal has info about downloading new version', () => {
|
||||
cy.get('.modal').contains('Download the new version')
|
||||
})
|
||||
|
||||
it('opens download link when Download is clicked', function () {
|
||||
cy.contains('Download the new version').click().then(() => {
|
||||
expect(this.ipc.externalOpen).to.be.calledWith('https://download.cypress.io/desktop')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('in project mode', function () {
|
||||
const npmCommand = `npm install --save-dev cypress@${NEW_VERSION}`
|
||||
const yarnCommand = `yarn upgrade cypress@${NEW_VERSION}`
|
||||
|
||||
beforeEach(function () {
|
||||
cy.stub(this.ipc, 'getOptions').resolves({ version: OLD_VERSION, projectRoot: '/foo/bar' })
|
||||
this.start()
|
||||
this.updaterCheck.resolve(NEW_VERSION)
|
||||
|
||||
cy.get('.footer .version').click()
|
||||
})
|
||||
|
||||
it('modal has info about upgrading via package manager', function () {
|
||||
cy.get('.modal').contains(npmCommand)
|
||||
cy.get('.modal').contains(yarnCommand)
|
||||
cy.percySnapshot()
|
||||
})
|
||||
|
||||
it('copies npm upgrade command to clipboard', function () {
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
.then(() => {
|
||||
expect(this.ipc.setClipboardText).to.be.calledWith(npmCommand)
|
||||
})
|
||||
})
|
||||
|
||||
it('changes npm upgrade button icon after copying', function () {
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
cy.contains(npmCommand).find('button i').should('have.class', 'fa-check')
|
||||
})
|
||||
|
||||
it('disables npm upgrade button after copying', function () {
|
||||
cy.contains(npmCommand).find('button').click().should('be.disabled')
|
||||
})
|
||||
|
||||
it('resets npm upgrade button after 5 seconds', function () {
|
||||
cy.clock()
|
||||
cy.contains(npmCommand).find('button').click()
|
||||
cy.tick(5000)
|
||||
cy.contains(npmCommand).find('button i').should('have.class', 'fa-copy')
|
||||
cy.contains(npmCommand).find('button').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('copies yarn upgrade command to clipboard', function () {
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
.then(() => {
|
||||
expect(this.ipc.setClipboardText).to.be.calledWith(yarnCommand)
|
||||
})
|
||||
})
|
||||
|
||||
it('changes yarn upgrade button icon after copying', function () {
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
cy.contains(yarnCommand).find('button i').should('have.class', 'fa-check')
|
||||
})
|
||||
|
||||
it('disables yarn upgrade button after copying', function () {
|
||||
cy.contains(yarnCommand).find('button').click().should('be.disabled')
|
||||
})
|
||||
|
||||
it('resets yarn upgrade button after 5 seconds', function () {
|
||||
cy.clock()
|
||||
cy.contains(yarnCommand).find('button').click()
|
||||
cy.tick(5000)
|
||||
cy.contains(yarnCommand).find('button i').should('have.class', 'fa-copy')
|
||||
cy.contains(yarnCommand).find('button').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('links to \'open\' doc on click of open command', function () {
|
||||
cy.contains('cypress open').click().then(() => {
|
||||
expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/how-to-open-cypress')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,11 @@ beforeEach(function () {
|
||||
})
|
||||
|
||||
Cypress.Commands.add('visitIndex', (options = {}) => {
|
||||
// disable livereload within the Cypress-loaded desktop GUI. it doesn't fully
|
||||
// reload the app because the stubbed out ipc calls don't work after the first
|
||||
// time, so it ends up a useless white page
|
||||
cy.route2({ path: /livereload/ }, '')
|
||||
|
||||
return cy.visit('/', options)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const BluebirdPromise = require('bluebird')
|
||||
|
||||
export const deferred = (Promise = BluebirdPromise) => {
|
||||
const deferred = {}
|
||||
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.resolve = resolve
|
||||
deferred.reject = reject
|
||||
})
|
||||
|
||||
return deferred
|
||||
}
|
||||
|
||||
export const deepClone = (obj) => {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
"dev": "yarn start-test 5005 cypress:open",
|
||||
"postinstall": "echo '@packages/desktop-gui needs: yarn build'",
|
||||
"start": "http-server -p 5005 dist",
|
||||
"watch": "yarn build -- --watch --progress"
|
||||
"watch": "yarn build --watch --progress"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/polyfill": "7.8.7",
|
||||
@@ -45,6 +45,7 @@
|
||||
"react-inspector": "5.0.1",
|
||||
"react-loader": "2.4.7",
|
||||
"react-select": "3.1.0",
|
||||
"react-transition-group": "4.4.1",
|
||||
"webpack": "4.35.3",
|
||||
"webpack-cli": "3.3.2"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import appStore from '../lib/app-store'
|
||||
import { useUpdateChecker } from '../update/use-update-checker'
|
||||
|
||||
import UpdateModal from '../update/update-modal'
|
||||
import UpdateNotice from '../update/update-notice'
|
||||
|
||||
const openChangelog = (e) => {
|
||||
e.target.blur()
|
||||
@@ -44,6 +45,7 @@ const Footer = observer(() => {
|
||||
</button>
|
||||
<button className='open-changelog' onClick={openChangelog}>Changelog</button>
|
||||
<UpdateModal show={state.showingModal} onClose={state.hideModal} />
|
||||
<UpdateNotice onOpenUpdatesModal={state.showModal} />
|
||||
</footer>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ class AppStore {
|
||||
@observable newVersion
|
||||
@observable version
|
||||
@observable localInstallNoticeDismissed = localData.get('local-install-notice-dimissed')
|
||||
@observable dismissedUpdateVersion = localData.get('dismissed-update-version')
|
||||
@observable error
|
||||
@observable proxyServer
|
||||
@observable proxyBypassList
|
||||
@@ -35,6 +36,10 @@ class AppStore {
|
||||
return this.version !== this.newVersion
|
||||
}
|
||||
|
||||
@computed get nonDismissedUpdateAvailable () {
|
||||
return this.updateAvailable && this.newVersion !== this.dismissedUpdateVersion
|
||||
}
|
||||
|
||||
@action set (props) {
|
||||
if (props.cypressEnv != null) this.cypressEnv = props.cypressEnv
|
||||
|
||||
@@ -58,6 +63,11 @@ class AppStore {
|
||||
localData.set('local-install-notice-dimissed', isDismissed)
|
||||
}
|
||||
|
||||
@action setDismissedUpdateVersion () {
|
||||
this.dismissedUpdateVersion = this.newVersion
|
||||
localData.set('dismissed-update-version', this.newVersion)
|
||||
}
|
||||
|
||||
@action setError (err) {
|
||||
this.error = err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import cs from 'classnames'
|
||||
import React from 'react'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
import { Portal } from '@packages/ui-components'
|
||||
|
||||
const Notification = ({ children, className, onClose, show }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<CSSTransition in={show} timeout={500} classNames='notification'>
|
||||
<div className={cs('notification', className)}>
|
||||
<div className='notification-wrap'>
|
||||
<div className='content'>
|
||||
{children}
|
||||
</div>
|
||||
<button className='notification-close' onClick={onClose}>
|
||||
<i className='fas fa-times' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
||||
@@ -0,0 +1,99 @@
|
||||
.notification {
|
||||
bottom: 4.8rem;
|
||||
padding-right: 1.2rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
z-index: 1020;
|
||||
|
||||
.notification-wrap {
|
||||
align-items: center;
|
||||
background: #252831;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
padding: 0.8rem 1.2rem 0.8rem 1.5rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
align-items: center;
|
||||
color: #EFF0F3;
|
||||
display: flex;
|
||||
font-size: 1.3rem;
|
||||
|
||||
i {
|
||||
color: #5393E9;
|
||||
font-size: 2.2rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8EBCF9;
|
||||
margin-left: 1rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #accdf9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #8A8C92;
|
||||
font-size: 2.4rem;
|
||||
margin-left: 3rem;
|
||||
padding: 0 0.4rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// transitions for react-transition-group
|
||||
// slide in from right on enter, fade/shrink away on exit
|
||||
&,
|
||||
&-enter {
|
||||
transform: translate(110%);
|
||||
}
|
||||
|
||||
&-enter {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
transform: translate(0);
|
||||
transition: transform 500ms ease-out;
|
||||
}
|
||||
|
||||
&-enter-done {
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
transform: translate(0);
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
transform: translate(0) scale(0.7);
|
||||
transition: all 300ms ease-out;
|
||||
}
|
||||
|
||||
&-exit-done {
|
||||
opacity: 0;
|
||||
transform: translate(110%);
|
||||
}
|
||||
}
|
||||
|
||||
.notification-exit-done.notification-exit-active {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.open-notice {
|
||||
position: fixed;
|
||||
bottom: 16rem;
|
||||
right: 1rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import { observer } from 'mobx-react'
|
||||
|
||||
import appStore from '../lib/app-store'
|
||||
import Notification from '../notifications/notification'
|
||||
|
||||
const UpdateNotice = observer(({ onOpenUpdatesModal }) => {
|
||||
const onClose = () => {
|
||||
appStore.setDismissedUpdateVersion()
|
||||
}
|
||||
|
||||
const onLearnMore = (e) => {
|
||||
e.preventDefault()
|
||||
appStore.setDismissedUpdateVersion()
|
||||
|
||||
onOpenUpdatesModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<Notification className='update-notice' show={appStore.nonDismissedUpdateAvailable} onClose={onClose}>
|
||||
<i className='fas fa-shipping-fast' />
|
||||
An update ({appStore.newVersion}) is available.{' '}
|
||||
<a href='#' onClick={onLearnMore}>Learn more</a>
|
||||
</Notification>
|
||||
)
|
||||
})
|
||||
|
||||
export default UpdateNotice
|
||||
@@ -2,6 +2,8 @@ export { default as BrowserIcon } from './browser-icon'
|
||||
|
||||
export { default as Dropdown } from './dropdown'
|
||||
|
||||
export { default as Portal } from './portal'
|
||||
|
||||
export { default as EditorPicker } from './file-opener/editor-picker'
|
||||
|
||||
export { default as EditorPickerModal } from './file-opener/editor-picker-modal'
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
let idNum = 0
|
||||
|
||||
const Portal = ({ children }) => {
|
||||
const [id, setId] = useState(null)
|
||||
|
||||
useEffect(() => () => {
|
||||
// remove element on unmount. use a try/catch because it's possible
|
||||
// the element was removed from the dom or the dom has been blown
|
||||
// away, which will cause `removeChild` to throw an exception
|
||||
try {
|
||||
document.removeChild(element)
|
||||
} catch (err) {} // eslint-disable-line no-empty
|
||||
}, [true])
|
||||
|
||||
if (!id) {
|
||||
setId(`cy-desktop-gui-portal-${idNum++}`)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
let element = document.getElementById(id)
|
||||
|
||||
if (!element) {
|
||||
element = document.createElement('div')
|
||||
element.id = id
|
||||
document.body.appendChild(element)
|
||||
}
|
||||
|
||||
return createPortal(children, element)
|
||||
}
|
||||
|
||||
export default Portal
|
||||
@@ -25797,6 +25797,16 @@ react-test-renderer@^16.0.0-0:
|
||||
react-is "^16.8.6"
|
||||
scheduler "^0.19.1"
|
||||
|
||||
react-transition-group@4.4.1, react-transition-group@^4.3.0:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-transition-group@^2.0.0, react-transition-group@^2.2.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
||||
@@ -25807,16 +25817,6 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0:
|
||||
prop-types "^15.6.2"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react-transition-group@^4.3.0:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
dom-helpers "^5.0.1"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react@16.13.1, react@^16.0.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
|
||||
Reference in New Issue
Block a user