From 5794e7c0d8e8aa25e3148f2fcc3f5658425120fe Mon Sep 17 00:00:00 2001 From: Ed Schiebel Date: Thu, 5 Sep 2019 20:00:20 -0400 Subject: [PATCH] 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 Product-Review: Ryan Shaw QA-Review: Jeremy Putnam --- app/jsx/editor/ExternalToolDialog.js | 4 +- .../__tests__/ExternalToolDialog.test.js | 3 +- app/models/context_external_tool.rb | 8 +- docker-compose.override.yml | 1 + packages/canvas-rce/package.json | 2 +- .../clickCallback.js | 45 +++++++ .../components/LtiToolsModal/ExpandoText.js | 101 +++++++++++++++ .../components/LtiToolsModal/LtiTool.js | 71 +++++++++++ .../__tests__/ExpandoText.test.js | 59 +++++++++ .../LtiToolsModal/__tests__/LtiTool.test.js | 56 +++++++++ .../__tests__/LtiToolsModal.test.js | 117 ++++++++++++++++++ .../components/LtiToolsModal/index.js | 97 +++++++++++++++ .../instructure_external_tools/plugin.js | 4 + .../ExternalToolsHelper.js | 4 +- .../initializeExternalTools.js | 7 +- spec/helpers/application_helper_spec.rb | 32 ++++- spec/models/context_external_tool_spec.rb | 16 +++ spec/selenium/rcs/pages/rce_next_page.rb | 4 + spec/selenium/rcs/rce_next_spec.rb | 36 ++++-- 19 files changed, 644 insertions(+), 23 deletions(-) create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/clickCallback.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/ExpandoText.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/LtiTool.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/ExpandoText.test.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiTool.test.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiToolsModal.test.js create mode 100644 packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/index.js diff --git a/app/jsx/editor/ExternalToolDialog.js b/app/jsx/editor/ExternalToolDialog.js index f812b60dd8e..77adcd65a9e 100644 --- a/app/jsx/editor/ExternalToolDialog.js +++ b/app/jsx/editor/ExternalToolDialog.js @@ -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}) diff --git a/app/jsx/editor/__tests__/ExternalToolDialog.test.js b/app/jsx/editor/__tests__/ExternalToolDialog.test.js index 3eaf38185ef..9f09758926b 100644 --- a/app/jsx/editor/__tests__/ExternalToolDialog.test.js +++ b/app/jsx/editor/__tests__/ExternalToolDialog.test.js @@ -68,7 +68,8 @@ function fakeEditor() { selection: { getContent: jest.fn() }, - getContent: jest.fn() + getContent: jest.fn(), + focus: () => {} } } diff --git a/app/models/context_external_tool.rb b/app/models/context_external_tool.rb index 2dbc77ff99a..ed755bfb9cf 100644 --- a/app/models/context_external_tool.rb +++ b/app/models/context_external_tool.rb @@ -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 diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 2f5c55c0d3e..20c7c7b79ca 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -43,6 +43,7 @@ services: environment: <<: *BASE-ENV VIRTUAL_HOST: .canvas.docker + HTTPS_METHOD: noredirect postgres: volumes: diff --git a/packages/canvas-rce/package.json b/packages/canvas-rce/package.json index 4de75e525d5..7932dfb9227 100644 --- a/packages/canvas-rce/package.json +++ b/packages/canvas-rce/package.json @@ -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", diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/clickCallback.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/clickCallback.js new file mode 100644 index 00000000000..35b8c57a469 --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/clickCallback.js @@ -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 . + */ + +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( + , container + ) + }) +} \ No newline at end of file diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/ExpandoText.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/ExpandoText.js new file mode 100644 index 00000000000..90367798db8 --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/ExpandoText.js @@ -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 . + */ + +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 ( + { + 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)} + > + + + + {descExpanded ? : } + + + + +
+ + + + + ) +} + +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' + } +}); \ No newline at end of file diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/LtiTool.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/LtiTool.js new file mode 100644 index 00000000000..519f622db75 --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/LtiTool.js @@ -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 . + */ + +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 ( + <> + { + onAction() + }} + onKeyDown={(e) => { + if (e.keyCode === 13 || e.keyCode === 32) { + onAction() + } + }} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + tabIndex="0" + > + + {title} + + {description && renderDescription(description)} + + ) + + function renderDescription(desc) { + return ( +
+ +
+ ) + } +} + +LtiTool.propTypes = { + title: String.isRequired, + image: string.isRequired, + onAction: func.isRequired, + description: string +} \ No newline at end of file diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/ExpandoText.test.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/ExpandoText.test.js new file mode 100644 index 00000000000..3705516375d --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/ExpandoText.test.js @@ -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 . + */ + +import React from 'react' +import {render, fireEvent} from '@testing-library/react' + +import ExpandoText from '../ExpandoText' + +describe('RCE Plugins > LtiTool', () => { + + function renderComponent(text) { + return render( +
+ ) +
+ ) + } + + 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() + }) +}) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiTool.test.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiTool.test.js new file mode 100644 index 00000000000..2c59256024b --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiTool.test.js @@ -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 . + */ + +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() + } + + 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() + }) +}) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiToolsModal.test.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiToolsModal.test.js new file mode 100644 index 00000000000..846d17b2454 --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/__tests__/LtiToolsModal.test.js @@ -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 . + */ + +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() + } + + 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() + }) +}) diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/index.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/index.js new file mode 100644 index 00000000000..4c20bd90739 --- /dev/null +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/components/LtiToolsModal/index.js @@ -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 . + */ + +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 ( + + + + {formatMessage('Close')} + + {formatMessage('Select App')} + + + {renderTools(props.ltiButtons)} + + + + + + ) + + function renderTools(ltiButtons) { + return ( + + {ltiButtons.sort((a, b) => a.title.localeCompare(b.title)).map((b, i) => { + return ( + + + { + b.onAction() + props.onDismiss() + }} + description={b.description} + /> + + + ) + })} + + ) + } + + +} + +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 +} \ No newline at end of file diff --git a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/plugin.js b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/plugin.js index aa253000832..5fbc18302d6 100644 --- a/packages/canvas-rce/src/rce/plugins/instructure_external_tools/plugin.js +++ b/packages/canvas-rce/src/rce/plugins/instructure_external_tools/plugin.js @@ -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); }, diff --git a/public/javascripts/tinymce_plugins/instructure_external_tools/ExternalToolsHelper.js b/public/javascripts/tinymce_plugins/instructure_external_tools/ExternalToolsHelper.js index bb6caf29b74..0b67f77f882 100644 --- a/public/javascripts/tinymce_plugins/instructure_external_tools/ExternalToolsHelper.js +++ b/public/javascripts/tinymce_plugins/instructure_external_tools/ExternalToolsHelper.js @@ -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}` } diff --git a/public/javascripts/tinymce_plugins/instructure_external_tools/initializeExternalTools.js b/public/javascripts/tinymce_plugins/instructure_external_tools/initializeExternalTools.js index 2cc2ee49dec..c08de412e01 100644 --- a/public/javascripts/tinymce_plugins/instructure_external_tools/initializeExternalTools.js +++ b/public/javascripts/tinymce_plugins/instructure_external_tools/initializeExternalTools.js @@ -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' diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2401fa69979..456ec076ca0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -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 => "

the description.

\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 diff --git a/spec/models/context_external_tool_spec.rb b/spec/models/context_external_tool_spec.rb index a64e3bb5c91..15cbaf05651 100644 --- a/spec/models/context_external_tool_spec.rb +++ b/spec/models/context_external_tool_spec.rb @@ -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 "

the first paragraph.

\n\n

the second paragraph.

\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 "

link text

\n" + end + end end end diff --git a/spec/selenium/rcs/pages/rce_next_page.rb b/spec/selenium/rcs/pages/rce_next_page.rb index 95b9fed2ff7..0bf2592232b 100644 --- a/spec/selenium/rcs/pages/rce_next_page.rb +++ b/spec/selenium/rcs/pages/rce_next_page.rb @@ -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 diff --git a/spec/selenium/rcs/rce_next_spec.rb b/spec/selenium/rcs/rce_next_spec.rb index 872b9d0dc9c..cd8a1445a55 100644 --- a/spec/selenium/rcs/rce_next_spec.rb +++ b/spec/selenium/rcs/rce_next_spec.rb @@ -573,25 +573,29 @@ 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", - :domain => "canvaslms.com", - :consumer_key => '12345', - :shared_secret => 'secret' + :name => "Commons", + :domain => "canvaslms.com", + :consumer_key => '12345', + :shared_secret => 'secret' }) @tool.set_extension_setting(:editor_button, { - :message_type => "ContentItemSelectionRequest", - :url => "http://www.example.com", - :icon_url => "https://lor.instructure.com/img/icon_commons.png", - :text => "Commons Favorites", - :enabled => "true", - :use_tray => "true" + :message_type => "ContentItemSelectionRequest", + :url => "http://www.example.com", + :icon_url => "https://lor.instructure.com/img/icon_commons.png", + :text => "Commons Favorites", + :enabled => "true", + :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