Add LTI tool modal
closes COREFE-253 test plan: - in a class with some LTI tools enabled, open a page with the rce - click on the external tools toolbar button > expect the Select App modal to open > expect the tools to be listed aphabetically > expect the icon, tool title, and first line of the tools's description displayed > expect that clicking on the description expands so you can read it all > expect the 'x' and Cancel buttons to close the modal - click on an app > expect the modal to close and the app's modal to open - select content from the app > expect it to be added to the rce's content > expect that you can open the modal and do it all again - in a course with no installed apps > expect the external tools toolbar button not to appear in the rce Change-Id: I6d798d08c4449f1c1e3a373057edb89af236a81e Reviewed-on: https://gerrit.instructure.com/208422 Tested-by: Jenkins Reviewed-by: Ryan Shaw <ryan@instructure.com> Product-Review: Ryan Shaw <ryan@instructure.com> QA-Review: Jeremy Putnam <jeremyp@instructure.com>
This commit is contained in:
parent
4133f48733
commit
5794e7c0d8
|
@ -54,7 +54,8 @@ export default class ExternalToolDialog extends React.Component {
|
|||
selection: PropTypes.shape({
|
||||
getContent: PropTypes.func.isRequired
|
||||
}),
|
||||
getContent: PropTypes.func.isRequired
|
||||
getContent: PropTypes.func.isRequired,
|
||||
focus: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
contextAssetString: PropTypes.string.isRequired,
|
||||
iframeAllowances: PropTypes.string.isRequired,
|
||||
|
@ -140,6 +141,7 @@ export default class ExternalToolDialog extends React.Component {
|
|||
|
||||
handleRemove = () => {
|
||||
this.setState({button: EMPTY_BUTTON})
|
||||
this.props.editor.focus()
|
||||
}
|
||||
|
||||
handleInfoAlertFocus = ev => this.setState({infoAlert: ev.target})
|
||||
|
|
|
@ -68,7 +68,8 @@ function fakeEditor() {
|
|||
selection: {
|
||||
getContent: jest.fn()
|
||||
},
|
||||
getContent: jest.fn()
|
||||
getContent: jest.fn(),
|
||||
focus: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -873,6 +873,7 @@ end
|
|||
|
||||
def self.editor_button_json(tools, context, user, session=nil)
|
||||
tools.select! {|tool| visible?(tool.editor_button['visibility'], user, context, session)}
|
||||
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({link_attributes: {target: '_blank'}}))
|
||||
tools.map do |tool|
|
||||
{
|
||||
:name => tool.label_for(:editor_button, I18n.locale),
|
||||
|
@ -882,7 +883,12 @@ end
|
|||
:canvas_icon_class => tool.editor_button(:canvas_icon_class),
|
||||
:width => tool.editor_button(:selection_width),
|
||||
:height => tool.editor_button(:selection_height),
|
||||
:use_tray => tool.editor_button(:use_tray) == "true"
|
||||
:use_tray => tool.editor_button(:use_tray) == "true",
|
||||
:description => if tool.description
|
||||
Sanitize.clean(markdown.render(tool.description), CanvasSanitize::SANITIZE)
|
||||
else
|
||||
""
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,6 +43,7 @@ services:
|
|||
environment:
|
||||
<<: *BASE-ENV
|
||||
VIRTUAL_HOST: .canvas.docker
|
||||
HTTPS_METHOD: noredirect
|
||||
|
||||
postgres:
|
||||
volumes:
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"integration-test": "nightwatch --env integration",
|
||||
"lint": "eslint \"src/**/*.js\" \"test/**/*.js\"",
|
||||
"lint:fix": "eslint --fix \"src/**/*.js\" \"test/**/*.js\"",
|
||||
"test": "Test cafe will be added back to test as part of CORE-2995",
|
||||
"_test": "Test cafe will be added back to test as part of CORE-2995",
|
||||
"test": "yarn test:mocha && yarn test:jest",
|
||||
"test:mocha": "BABEL_ENV=test-node mocha 'test/**/*.test.js' --require @instructure/canvas-theme --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
"test:mocha:one": "BABEL_ENV=test-node mocha --require @instructure/canvas-theme --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
export default function clickCallback(ed, ltiButtons) {
|
||||
|
||||
return import('./components/LtiToolsModal').then(({LtiToolsModal}) => {
|
||||
let container = document.querySelector('.canvas-rce-upload-container')
|
||||
if (!container) {
|
||||
container = document.createElement('div')
|
||||
container.className = 'canvas-rce-upload-container'
|
||||
document.body.appendChild(container)
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
ReactDOM.unmountComponentAtNode(container)
|
||||
ed.focus()
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<LtiToolsModal
|
||||
editor={ed}
|
||||
onDismiss={handleDismiss}
|
||||
ltiButtons={ltiButtons}
|
||||
/>, container
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {string} from 'prop-types'
|
||||
import { StyleSheet, css } from "aphrodite";
|
||||
|
||||
import {Text} from '@instructure/ui-elements'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import {IconArrowOpenDownLine, IconArrowOpenEndLine} from '@instructure/ui-icons'
|
||||
|
||||
export default function ExpandoText(props) {
|
||||
const [descExpanded, setDescExpanded] = useState(false)
|
||||
const [focused, setFocused] = useState(false)
|
||||
|
||||
const {text} = props
|
||||
return (
|
||||
<View
|
||||
as="button"
|
||||
className={css(styles.toggleButton)}
|
||||
type="button"
|
||||
position="relative"
|
||||
aria-expanded={descExpanded}
|
||||
focused={focused}
|
||||
onClick={(event) => {
|
||||
if(event.target.tagName !== 'A' || event.target.tagName !== 'BUTTON') {
|
||||
// let the user click on links and buttons
|
||||
setDescExpanded(!descExpanded)
|
||||
}
|
||||
}}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
>
|
||||
<span style={{display: 'flex', alignItems: 'start'}}>
|
||||
<span style={{marginRight: '.25rem', display: 'inline-block'}}>
|
||||
<Text color="secondary">
|
||||
{descExpanded ? <IconArrowOpenDownLine/> : <IconArrowOpenEndLine/>}
|
||||
</Text>
|
||||
</span>
|
||||
<span style={{flexGrow: '1', minWidth: '10rem'}}>
|
||||
<Text as="span" color="secondary">
|
||||
<div
|
||||
className={css(styles.descriptionText, descExpanded ? null : styles.overflow)}
|
||||
dangerouslySetInnerHTML={{__html: text}}
|
||||
/>
|
||||
</Text>
|
||||
</span>
|
||||
</span>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
ExpandoText.propTypes = {
|
||||
text: string.isRequired
|
||||
}
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
toggleButton: {
|
||||
background: 'transparent',
|
||||
borderStyle: 'none',
|
||||
display: 'block',
|
||||
padding: '.25rem',
|
||||
textAlign: 'start',
|
||||
maxWidth: '100%'
|
||||
},
|
||||
descriptionText: {
|
||||
overflow: 'hidden',
|
||||
lineHeight: '1.2rem',
|
||||
p: {
|
||||
margin: '1rem 0'
|
||||
},
|
||||
':nth-child(1n)> :first-child': {
|
||||
marginTop: '0',
|
||||
display: 'inline-block'
|
||||
},
|
||||
':nth-child(1n)> :last-child': {
|
||||
marginBottom: '0'
|
||||
}
|
||||
},
|
||||
overflow: {
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
height: '1.2rem',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import {func, string} from 'prop-types'
|
||||
import {Text} from '@instructure/ui-elements'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import ExpandoText from './ExpandoText'
|
||||
|
||||
|
||||
export default function LtiTool(props) {
|
||||
const [focused, setFocused] = useState(false)
|
||||
const {title, image, description, onAction} = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
as="div"
|
||||
focused={focused}
|
||||
role="button"
|
||||
position="relative"
|
||||
margin="none none small"
|
||||
onClick={() => {
|
||||
onAction()
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13 || e.keyCode === 32) {
|
||||
onAction()
|
||||
}
|
||||
}}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
tabIndex="0"
|
||||
>
|
||||
<span style={{marginRight: '.5rem'}}><img src={image} alt="" /></span>
|
||||
<Text weight="bold">{title}</Text>
|
||||
</View>
|
||||
{description && renderDescription(description)}
|
||||
</>
|
||||
)
|
||||
|
||||
function renderDescription(desc) {
|
||||
return (
|
||||
<div style={{margin: '0 1.5rem', position: 'relative', boxSizing: 'content-box'}}>
|
||||
<ExpandoText text={desc}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LtiTool.propTypes = {
|
||||
title: String.isRequired,
|
||||
image: string.isRequired,
|
||||
onAction: func.isRequired,
|
||||
description: string
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
|
||||
import ExpandoText from '../ExpandoText'
|
||||
|
||||
describe('RCE Plugins > LtiTool', () => {
|
||||
|
||||
function renderComponent(text) {
|
||||
return render(
|
||||
<div style={{width: '10rem'}}>
|
||||
<ExpandoText text={text} />)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
it('renters right-arrow when first rendered', () => {
|
||||
const {container} = renderComponent('hello world')
|
||||
expect(container.querySelector('svg[name="IconArrowOpenEnd"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the text', () => {
|
||||
const {getByText} = renderComponent('hello world')
|
||||
expect(getByText("hello world")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the down-arrow when expanded', () => {
|
||||
const {container} = renderComponent('hello world')
|
||||
const arrowButton = container.querySelector('svg[name="IconArrowOpenEnd"]')
|
||||
fireEvent.click(arrowButton)
|
||||
expect(container.querySelector('svg[name="IconArrowOpenDown"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the right-arrow when collapsed', () => {
|
||||
const {container} = renderComponent('hello world')
|
||||
fireEvent.click(container.querySelector('svg[name="IconArrowOpenEnd"]'))
|
||||
const downButton = container.querySelector('svg[name="IconArrowOpenDown"]')
|
||||
expect(downButton).toBeInTheDocument()
|
||||
fireEvent.click(downButton)
|
||||
expect(container.querySelector('svg[name="IconArrowOpenEnd"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import LtiTool from '../LtiTool'
|
||||
|
||||
describe('RCE Plugins > LtiTool', () => {
|
||||
|
||||
function getProps(override={}) {
|
||||
const props = {
|
||||
title: "Tool 1",
|
||||
id: 1,
|
||||
description: "This is tool 1.",
|
||||
image: "tool1/icon.png",
|
||||
onAction: () => {},
|
||||
...override
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
function renderComponent(toolprops) {
|
||||
return render(<LtiTool {...getProps(toolprops)} />)
|
||||
}
|
||||
|
||||
it('renters the tool title', () => {
|
||||
const {getByText} = renderComponent()
|
||||
expect(getByText("Tool 1")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renters the tool image', () => {
|
||||
const {container} = renderComponent()
|
||||
expect(container.querySelector('img[src="tool1/icon.png"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the tool description', () => {
|
||||
const {getByText} = renderComponent()
|
||||
expect(getByText("This is tool 1.")).toBeInTheDocument()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
import {LtiToolsModal} from '../index'
|
||||
|
||||
describe('RCE Plugins > LtiToolModal', () => {
|
||||
|
||||
function getProps(override={}) {
|
||||
const props = {
|
||||
onDismiss: () => {},
|
||||
ltiButtons: [
|
||||
{
|
||||
title: "Tool 1",
|
||||
id: 1,
|
||||
description: "This is tool 1.",
|
||||
image: "tool1/icon.png",
|
||||
onAction: () => {}
|
||||
},
|
||||
{
|
||||
title: "Tool 2",
|
||||
id: 2,
|
||||
description: "This is tool 2",
|
||||
image: "/tool2/image.png",
|
||||
onAction: () => {}
|
||||
},
|
||||
{
|
||||
title: "Tool 3",
|
||||
id: 3,
|
||||
image: "https://www.edu-apps.org/assets/lti_public_resources/tool3.png",
|
||||
onAction: () => {}
|
||||
}
|
||||
],
|
||||
...override
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
function renderComponent(modalprops) {
|
||||
return render(<LtiToolsModal {...getProps(modalprops)} />)
|
||||
}
|
||||
|
||||
it('is labeled "Select App"', () => {
|
||||
const {getByLabelText} = renderComponent()
|
||||
expect(getByLabelText("LTI Tools")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has heading "Select App"', () => {
|
||||
const {getByText} = renderComponent()
|
||||
expect(getByText("Select App")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the 3 tools', () => {
|
||||
const {baseElement, getByText} = renderComponent()
|
||||
expect(getByText('Tool 1')).toBeInTheDocument()
|
||||
expect(getByText('This is tool 1.')).toBeInTheDocument()
|
||||
expect(baseElement.querySelector('img[src="tool1/icon.png"]')).toBeInTheDocument()
|
||||
expect(getByText('Tool 2')).toBeInTheDocument()
|
||||
expect(getByText('This is tool 2')).toBeInTheDocument()
|
||||
expect(baseElement.querySelector('img[src="/tool2/image.png"]')).toBeInTheDocument()
|
||||
expect(getByText('Tool 3')).toBeInTheDocument()
|
||||
expect(baseElement.querySelector('img[src="https://www.edu-apps.org/assets/lti_public_resources/tool3.png"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onDismiss when clicking Cancel', () => {
|
||||
const handleDismiss = jest.fn()
|
||||
const {getByText} = renderComponent({onDismiss: handleDismiss})
|
||||
const cancelButton = getByText('Cancel')
|
||||
cancelButton.click()
|
||||
expect(handleDismiss).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onDismiss when clicking the close button', () => {
|
||||
const handleDismiss = jest.fn()
|
||||
const {getByText} = renderComponent({onDismiss: handleDismiss})
|
||||
const closeButton = getByText('Close')
|
||||
closeButton.click()
|
||||
expect(handleDismiss).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls onAction when clicking a tool', () => {
|
||||
const doAction = jest.fn()
|
||||
const onDismiss = jest.fn()
|
||||
const {getByText} = renderComponent({
|
||||
onDismiss,
|
||||
ltiButtons: [
|
||||
{
|
||||
title: "Tool 1",
|
||||
id: 1,
|
||||
description: "This is tool 1.",
|
||||
image: "tool1/icon.png",
|
||||
onAction: doAction
|
||||
}
|
||||
]})
|
||||
const tool1 = getByText('Tool 1')
|
||||
tool1.click()
|
||||
expect(doAction).toHaveBeenCalled()
|
||||
expect(onDismiss).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {func, arrayOf, oneOfType, number, shape, string} from 'prop-types'
|
||||
import {Modal} from '@instructure/ui-overlays'
|
||||
import {Button, CloseButton} from '@instructure/ui-buttons'
|
||||
import {Heading, List} from '@instructure/ui-elements'
|
||||
import {View} from '@instructure/ui-layout'
|
||||
import formatMessage from '../../../../../format-message'
|
||||
import LtiTool from './LtiTool'
|
||||
|
||||
// TODO: we really need a way for the client to pass this to the RCE
|
||||
const getLiveRegion=() => document.getElementById('flash_screenreader_holder')
|
||||
|
||||
export function LtiToolsModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
data-mce-component
|
||||
liveRegion={getLiveRegion}
|
||||
size="medium"
|
||||
label={formatMessage('LTI Tools')}
|
||||
onDismiss={props.onDismiss}
|
||||
open
|
||||
shouldCloseOnDocumentClick
|
||||
>
|
||||
<Modal.Header>
|
||||
<CloseButton placement="end" offset="medium" onClick={props.onDismiss}>
|
||||
{formatMessage('Close')}
|
||||
</CloseButton>
|
||||
<Heading>{formatMessage('Select App')}</Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{renderTools(props.ltiButtons)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={props.onDismiss}>{formatMessage('Cancel')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
function renderTools(ltiButtons) {
|
||||
return (
|
||||
<List variant="unstyled">
|
||||
{ltiButtons.sort((a, b) => a.title.localeCompare(b.title)).map((b, i) => {
|
||||
return (
|
||||
<List.Item key={b.id}>
|
||||
<View
|
||||
as="div"
|
||||
borderWidth={i === 0 ? "small none" : "none none small none"}
|
||||
padding="medium"
|
||||
>
|
||||
<LtiTool
|
||||
title={b.title}
|
||||
image={b.image}
|
||||
onAction={() => {
|
||||
b.onAction()
|
||||
props.onDismiss()
|
||||
}}
|
||||
description={b.description}
|
||||
/>
|
||||
</View>
|
||||
</List.Item>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
LtiToolsModal.propTypes = {
|
||||
ltiButtons: arrayOf(shape({
|
||||
description: string.isRequired,
|
||||
id: oneOfType([string, number]).isRequired,
|
||||
image: string.isRequired,
|
||||
onAction: func.isRequired,
|
||||
title: string.isRequired
|
||||
})),
|
||||
onDismiss: func.isRequired
|
||||
}
|
|
@ -18,10 +18,14 @@
|
|||
|
||||
import dispatchInitEvent from "./dispatchInitEvent";
|
||||
import {IconLtiLine} from '@instructure/ui-icons/es/svg'
|
||||
import clickCallback from './clickCallback'
|
||||
|
||||
|
||||
tinymce.create("tinymce.plugins.InstructureExternalTools", {
|
||||
init(ed, url) {
|
||||
document.addEventListener('tinyRCE/onExternalTools', (event) => {
|
||||
clickCallback(ed, event.detail.ltiButtons)
|
||||
})
|
||||
ed.ui.registry.addIcon('lti', IconLtiLine.src)
|
||||
dispatchInitEvent(ed, document, url);
|
||||
},
|
||||
|
|
|
@ -54,9 +54,9 @@ export default {
|
|||
classes: 'widget btn instructure_external_tool_button'
|
||||
}
|
||||
if (ENV.use_rce_enhancements) {
|
||||
config.text = config.title
|
||||
config.id = button.id
|
||||
config.onAction = () => editor.execCommand(`instructureExternalButton${button.id}`)
|
||||
config.type = 'menuitem'
|
||||
config.description = button.description
|
||||
} else {
|
||||
config.cmd = `instructureExternalButton${button.id}`
|
||||
}
|
||||
|
|
|
@ -82,9 +82,10 @@ const ExternalToolsPlugin = {
|
|||
}
|
||||
}
|
||||
if (ltiButtons.length && ENV.use_rce_enhancements) {
|
||||
ed.ui.registry.addMenuButton('lti_tool_dropdown', {
|
||||
fetch(callback) {
|
||||
callback(ltiButtons)
|
||||
ed.ui.registry.addButton('lti_tool_dropdown', {
|
||||
onAction: () => {
|
||||
const ev = new CustomEvent('tinyRCE/onExternalTools', {detail: {ltiButtons}})
|
||||
document.dispatchEvent(ev)
|
||||
},
|
||||
icon: 'lti',
|
||||
tooltip: 'LTI Tools'
|
||||
|
|
|
@ -516,12 +516,28 @@ describe ApplicationHelper do
|
|||
it "should return hash of tools if in group" do
|
||||
@course = course_model
|
||||
@group = @course.groups.create!(:name => "some group")
|
||||
tool = @course.context_external_tools.new(:name => "bob", :consumer_key => "test", :shared_secret => "secret", :url => "http://example.com")
|
||||
tool = @course.context_external_tools.new(
|
||||
:name => "bob",
|
||||
:consumer_key => "test",
|
||||
:shared_secret => "secret",
|
||||
:url => "http://example.com",
|
||||
:description => "the description."
|
||||
)
|
||||
tool.editor_button = {:url => "http://example.com", :icon_url => "http://example.com", :canvas_icon_class => 'icon-commons'}
|
||||
tool.save!
|
||||
@context = @group
|
||||
|
||||
expect(editor_buttons).to eq([{:name=>"bob", :id=>tool.id, :url=>"http://example.com", :icon_url=>"http://example.com", :canvas_icon_class => 'icon-commons', :width=>800, :height=>400, :use_tray => false}])
|
||||
expect(editor_buttons).to eq([{
|
||||
:name=>"bob",
|
||||
:id=>tool.id,
|
||||
:url=>"http://example.com",
|
||||
:icon_url=>"http://example.com",
|
||||
:canvas_icon_class => 'icon-commons',
|
||||
:width=>800,
|
||||
:height=>400,
|
||||
:use_tray => false,
|
||||
:description => "<p>the description.</p>\n"
|
||||
}])
|
||||
end
|
||||
|
||||
it "should return hash of tools if in course" do
|
||||
|
@ -532,7 +548,17 @@ describe ApplicationHelper do
|
|||
allow(controller).to receive(:group_external_tool_path).and_return('http://dummy')
|
||||
@context = @course
|
||||
|
||||
expect(editor_buttons).to eq([{:name=>"bob", :id=>tool.id, :url=>"http://example.com", :icon_url=>"http://example.com", :canvas_icon_class => 'icon-commons', :width=>800, :height=>400, :use_tray => false}])
|
||||
expect(editor_buttons).to eq([{
|
||||
:name=>"bob",
|
||||
:id=>tool.id,
|
||||
:url=>"http://example.com",
|
||||
:icon_url=>"http://example.com",
|
||||
:canvas_icon_class => 'icon-commons',
|
||||
:width=>800,
|
||||
:height=>400,
|
||||
:use_tray => false,
|
||||
:description => ""
|
||||
}])
|
||||
end
|
||||
|
||||
it "should not include tools from the domain_root_account for users" do
|
||||
|
|
|
@ -1624,5 +1624,21 @@ describe ContextExternalTool do
|
|||
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
|
||||
expect(json[0][:use_tray]).to eq true
|
||||
end
|
||||
|
||||
describe 'includes the description' do
|
||||
it 'parsed into HTML' do
|
||||
tool.editor_button = {}
|
||||
tool.description = "the first paragraph.\n\nthe second paragraph."
|
||||
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
|
||||
expect(json[0][:description]).to eq "<p>the first paragraph.</p>\n\n<p>the second paragraph.</p>\n"
|
||||
end
|
||||
|
||||
it 'with target="_blank" on links' do
|
||||
tool.editor_button = {}
|
||||
tool.description = "[link text](http://the.url)"
|
||||
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
|
||||
expect(json[0][:description]).to eq "<p><a href=\"http://the.url\" target=\"_blank\">link text</a></p>\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -146,6 +146,10 @@ module RCENextPage
|
|||
possibly_hidden_toolbar_button('button[aria-label="LTI Tools"')
|
||||
end
|
||||
|
||||
def lti_tools_modal
|
||||
f('[role="dialog"][aria-label="LTI Tools"]')
|
||||
end
|
||||
|
||||
def course_images
|
||||
f('[role="menuitem"][title="Course Images"]')
|
||||
end
|
||||
|
|
|
@ -573,8 +573,10 @@ describe "RCE next tests" do
|
|||
driver.switch_to.default_content
|
||||
expect(f('.tox-pop__dialog button[title="Show link options"]')).to eq(driver.switch_to.active_element)
|
||||
end
|
||||
end
|
||||
|
||||
it "should display lti icon with a tool enabled for the course" do
|
||||
describe 'lti tool integration' do
|
||||
before(:each) do
|
||||
# set up the lti tool
|
||||
@tool = Account.default.context_external_tools.new({
|
||||
:name => "Commons",
|
||||
|
@ -591,7 +593,9 @@ describe "RCE next tests" do
|
|||
:use_tray => "true"
|
||||
})
|
||||
@tool.save!
|
||||
end
|
||||
|
||||
it "should display lti icon with a tool enabled for the course" do
|
||||
page_title = "Page1"
|
||||
create_wiki_page_with_embedded_image(page_title)
|
||||
|
||||
|
@ -599,6 +603,16 @@ describe "RCE next tests" do
|
|||
|
||||
expect(lti_tools_button).to be_displayed
|
||||
end
|
||||
|
||||
it "should display the lti tool modal" do
|
||||
page_title = "Page1"
|
||||
create_wiki_page_with_embedded_image(page_title)
|
||||
|
||||
visit_existing_wiki_edit(@course, page_title)
|
||||
lti_tools_button.click
|
||||
|
||||
expect(lti_tools_modal).to be_displayed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue