Skip to content

Commit

Permalink
feat(protocol-designer): create modal component to show announcements (
Browse files Browse the repository at this point in the history
…#5091)

* feat(protocol-designer): create modal component to show announcements for new versions

closes #4995

* add tests

* update styling of modal to match design shape

* make version match pd version instead of run app

* update modal to match new styling

* move announcement key to persist file with other keys
  • Loading branch information
jen-fong authored Mar 2, 2020
1 parent 712dd62 commit 225c12a
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 19 deletions.
65 changes: 65 additions & 0 deletions protocol-designer/src/__tests__/persist.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @flow

import * as persist from '../persist'

describe('persist', () => {
describe('getLocalStorageItem', () => {
let getItemMock
beforeEach(() => {
getItemMock = jest.spyOn(
Object.getPrototypeOf(global.localStorage),
'getItem'
)
})

afterEach(() => {
jest.clearAllMocks()
})

test('retrieves localStorage data by key and parses data when it exists', () => {
const value = { test: 'some value' }
getItemMock.mockReturnValue(JSON.stringify(value))

const result = persist.getLocalStorageItem('key')

expect(result).toEqual(value)
})

test('returns undefined when localStorage could not be retrieved for key given', () => {
getItemMock.mockImplementation(() => {
throw new Error('something went wrong!')
})

const result = persist.getLocalStorageItem('key')

expect(result).toBeUndefined()
})
})

describe('setLocalStorageItem', () => {
let setItemMock
beforeEach(() => {
jest.clearAllMocks()
setItemMock = jest.spyOn(
Object.getPrototypeOf(global.localStorage),
'setItem'
)
})

afterEach(() => {
jest.clearAllMocks()
})

test('adds prefix to key sets localStorage item by key', () => {
const value = { a: 'a', b: 'b' }
setItemMock.mockReturnValue(undefined)

persist.setLocalStorageItem('key', value)

expect(setItemMock).toHaveBeenCalledWith(
'root.key',
JSON.stringify(value)
)
})
})
})
10 changes: 6 additions & 4 deletions protocol-designer/src/components/ProtocolEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ import * as React from 'react'
import cx from 'classnames'
import { DragDropContext } from 'react-dnd'
import MouseBackEnd from 'react-dnd-mouse-backend'
import { PrereleaseModeIndicator } from './PrereleaseModeIndicator'
import { ConnectedNav } from '../containers/ConnectedNav'
import { ConnectedSidebar } from '../containers/ConnectedSidebar'
import { ConnectedTitleBar } from '../containers/ConnectedTitleBar'
import { ConnectedMainPanel } from '../containers/ConnectedMainPanel'
import { PortalRoot as MainPageModalPortalRoot } from '../components/portals/MainPageModalPortal'
import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from '../ui/steps'
import { PrereleaseModeIndicator } from './PrereleaseModeIndicator'
import { PortalRoot as TopPortalRoot } from './portals/TopPortal'
import { NewFileModal } from './modals/NewFileModal'
import { FileUploadMessageModal } from './modals/FileUploadMessageModal'
import { LabwareUploadMessageModal } from './modals/LabwareUploadMessageModal'
import { GateModal } from './modals/GateModal'
import { PortalRoot as MainPageModalPortalRoot } from '../components/portals/MainPageModalPortal'
import { PortalRoot as TopPortalRoot } from './portals/TopPortal'
import { MAIN_CONTENT_FORCED_SCROLL_CLASSNAME } from '../ui/steps'
import { AnnouncementModal } from './modals/AnnouncementModal'
import styles from './ProtocolEditor.css'

const showGateModal =
Expand All @@ -39,6 +40,7 @@ function ProtocolEditorComponent() {
MAIN_CONTENT_FORCED_SCROLL_CLASSNAME
)}
>
<AnnouncementModal />
<NewFileModal showProtocolFields />
<FileUploadMessageModal />
<LabwareUploadMessageModal />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@import '@opentrons/components';

.announcement_modal {
border-radius: 0;
}

.modal_contents {
@apply --font-body-2-dark;

border-radius: 0;
padding: 0;
}

.modal_contents p {
margin-bottom: 1rem;
}

.modules_diagrams_row {
display: flex;
justify-content: center;
align-items: center;

/*
Keep image height at a specific ratio by shrinking the available space the
image has to take up
*/
padding-left: 19.294%;
padding-right: 19.294%;
}

.modules_diagram {
width: 100%;
height: 100%;
}

.separator {
color: var(--c-light-gray);
}

.announcement_body {
padding: 2.5rem 1.5rem 1.5rem 1.5rem;
}

.announcement_message {
margin-bottom: 2.5rem;
}

.announcement_heading {
@apply --font-header-dark;

display: flex;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @flow

import React from 'react'
import { shallow } from 'enzyme'
import { Modal, OutlineButton } from '@opentrons/components'
import * as persist from '../../../../persist'
import { AnnouncementModal } from '../'
import * as announcements from '../announcements'
import type { Announcement } from '../announcements'

jest.mock('../../../../persist.js')

describe('AnnouncementModal', () => {
const announcementKey = 'newType'
const getLocalStorageItemMock: JestMockFn<[string], mixed> =
persist.getLocalStorageItem

const announcementsMock: {
announcements: Array<Announcement>,
} = announcements

beforeEach(() => {
getLocalStorageItemMock.mockReturnValue(announcementKey)
})

test('modal is not shown when announcement has been shown before', () => {
announcementsMock.announcements = [
{
image: null,
heading: 'a test header',
message: 'test',
announcementKey,
},
]

const wrapper = shallow(<AnnouncementModal />)

expect(wrapper.find(Modal)).toHaveLength(0)
})

test('announcement is shown when user has not seen it before', () => {
announcementsMock.announcements = [
{
image: null,
heading: 'a test header',
message: 'brand new spanking feature',
announcementKey: 'newPipette',
},
]

const wrapper = shallow(<AnnouncementModal />)
const modal = wrapper.find(Modal)

expect(modal).toHaveLength(1)
expect(modal.html()).toContain('brand new spanking feature')
})

test('latest announcement is always shown', () => {
announcementsMock.announcements = [
{
image: null,
heading: 'a first header',
message: 'first announcement',
announcementKey,
},
{
image: null,
heading: 'a second header',
message: 'second announcement',
announcementKey: 'newPipette',
},
]

const wrapper = shallow(<AnnouncementModal />)
const modal = wrapper.find(Modal)

expect(modal).toHaveLength(1)
expect(modal.html()).toContain('second announcement')
})

test('optional image component is displayed when exists', () => {
announcementsMock.announcements = [
{
image: <img src="test.jpg" />,
heading: 'a test header',
message: 'brand new spanking feature',
announcementKey: 'newFeature',
},
]

const wrapper = shallow(<AnnouncementModal />)
const image = wrapper.find('img')
expect(image).toHaveLength(1)
})

test('button click saves announcement announcementKey to localStorage and closes modal', () => {
const newAnnouncementKey = 'newFeature'
announcementsMock.announcements = [
{
image: null,
heading: 'a test header',
message: 'brand new spanking feature',
announcementKey: newAnnouncementKey,
},
]

const wrapper = shallow(<AnnouncementModal />)
const button = wrapper.find(OutlineButton)
button.simulate('click')

expect(persist.setLocalStorageItem).toHaveBeenCalledWith(
persist.localStorageAnnouncementKey,
newAnnouncementKey
)
expect(wrapper.find(Modal)).toHaveLength(0)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @flow

import * as React from 'react'
import styles from './AnnouncementModal.css'

export type Announcement = {|
announcementKey: string,
image: React.Node | null,
heading: string,
message: React.Node,
|}

export const announcements: Array<Announcement> = [
{
announcementKey: 'modulesRequireRunAppUpdate',
image: (
<div className={styles.modules_diagrams_row}>
<img
className={styles.modules_diagram}
src={require('../../../images/modules/magdeck_tempdeck_combined.png')}
/>
</div>
),
heading: "We've updated the Protocol Designer",
message: (
<>
<p>
Protocol Designer BETA now supports Temperature and Magnetic modules.
</p>

<p>
Note: Protocols with modules{' '}
<strong>may require an app and robot update to run</strong>. You will
need to have the OT-2 app and robot on the latest versions (
<strong>3.17 and higher</strong>).
</p>
</>
),
},
]
61 changes: 61 additions & 0 deletions protocol-designer/src/components/modals/AnnouncementModal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// @flow

import React, { useState } from 'react'
import cx from 'classnames'
import { Modal, OutlineButton } from '@opentrons/components'
import { i18n } from '../../../localization'
import {
setLocalStorageItem,
getLocalStorageItem,
localStorageAnnouncementKey,
} from '../../../persist'
import modalStyles from '../modal.css'
import { announcements } from './announcements'
import styles from './AnnouncementModal.css'

export const AnnouncementModal = () => {
const { announcementKey, message, heading, image } = announcements[
announcements.length - 1
]

const userHasNotSeenAnnouncement =
getLocalStorageItem(localStorageAnnouncementKey) !== announcementKey

const [showAnnouncementModal, setShowAnnouncementModal] = useState<boolean>(
userHasNotSeenAnnouncement
)

const handleClick = () => {
setLocalStorageItem(localStorageAnnouncementKey, announcementKey)
setShowAnnouncementModal(false)
}

return (
<>
{showAnnouncementModal && (
<Modal
className={cx(modalStyles.modal, styles.announcement_modal)}
contentsClassName={styles.modal_contents}
>
{image && (
<>
{image}
<hr className={styles.separator} />
</>
)}

<div className={styles.announcement_body}>
<h3 className={styles.announcement_heading}>{heading}</h3>
<div className={styles.announcement_message}>{message}</div>

<div className={modalStyles.button_row}>
<OutlineButton onClick={handleClick}>
{i18n.t('button.got_it')}
</OutlineButton>
</div>
</div>
</Modal>
)}
</>
)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion protocol-designer/src/localization/en/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"save": "save",
"swap": "swap",
"yes": "yes",
"upload_custom_labware": "Upload custom labware"
"upload_custom_labware": "Upload custom labware",
"got_it": "Got It!"
}
Loading

0 comments on commit 225c12a

Please sign in to comment.