Retire old jQueryUI help dialog

Closes FOO-4518
flag=none

The original Canvas help was implemented via some now very
old code using jQuery and jQueryUI to create a help dialog.
Since then most displays of the help information (particularly
the display on the "Help Tray" which slides out from the
side navigation after login) have been reimplemented in
React and InstUI. The only holdout was the help dialog
available from the Canvas login screen. Time to get rid of
all that.

This commit removes all of the old jQuery help dialog and
replaces it with the existing React/InstUI help displays.

Test plan:
* be on the login screen
* click on the "Help" link
* you should see a help dialog pop up just like always
  but now it will look just like the side nav help tray
* you agree that everything the "old" login help
  dialog provided is still available

Change-Id: I367c396d6dc99bf01362e2fa4a4c8df4a3a2ea9f
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/350254
QA-Review: Charley Kline <ckline@instructure.com>
Product-Review: Charley Kline <ckline@instructure.com>
Reviewed-by: August Thornton <august@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Charley Kline 2024-06-14 13:56:17 -05:00
parent aeaf4bc879
commit c5b024ad71
22 changed files with 128 additions and 372 deletions

View File

@ -28,7 +28,7 @@ describe "help dialog" do
get("/login")
f("#footer .help_dialog_trigger").click
wait_for_ajaximations
expect(f("#help-dialog-options")).to be_displayed
expect(f('[data-testid="login-help-close-button"]')).to be_displayed
end
it "no longer shows a browser warning for IE" do

View File

@ -40,7 +40,7 @@ import {
IconHomeLine,
} from '@instructure/ui-icons'
import {useScope as useI18nScope} from '@canvas/i18n'
import HelpDialog from './HelpDialog/index'
import HelpDialog from '@canvas/help-dialog'
import {Link} from '@instructure/ui-link'
import CoursesList from './lists/CoursesList'
import GroupsList from './lists/GroupsList'

View File

@ -20,7 +20,7 @@ import {useScope as useI18nScope} from '@canvas/i18n'
import React from 'react'
import {View} from '@instructure/ui-view'
import {Heading} from '@instructure/ui-heading'
import HelpDialog from '../HelpDialog/index'
import HelpDialog from '@canvas/help-dialog'
import ReleaseNotesList from '../lists/ReleaseNotesList'
const I18n = useI18nScope('HelpTray')

View File

@ -214,12 +214,13 @@ if (ENV.badge_counts) {
import('./boot/initializers/showBadgeCounts')
}
// Load and then display the Canvas help dialog if the user has requested it
// Decorate the help link with the React/InstUI dialog from the navigation sidenav
async function openHelpDialog(event: Event): Promise<void> {
const helpLink = event.target as Element
event.preventDefault()
try {
const {default: helpDialog} = await import('@canvas/common/enableHelpDialog')
helpDialog.open()
const {renderLoginHelp} = await import('@canvas/help-dialog')
renderLoginHelp(helpLink)
} catch (e) {
/* eslint-disable no-console */
console.error('Help dialog could not be displayed')

View File

@ -1,85 +0,0 @@
/*
* Copyright (C) 2011 - 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 $ from 'jquery'
import 'jquery-migrate'
import helpDialog from '../enableHelpDialog'
import fakeENV from '@canvas/test-utils/fakeENV'
import 'jquery-tinypubsub'
// more tests are in spec/selenium/help_dialog_spec.rb
QUnit.module('HelpDialog', {
setup() {
fakeENV.setup({help_link_name: 'Links'})
helpDialog.animateDuration = 0
this.clock = sinon.useFakeTimers()
this.server = sinon.fakeServer.create()
this.server.respondWith('/help_links', '[]')
return this.server.respondWith('/api/v1/courses.json', '[]')
},
teardown() {
fakeENV.teardown()
this.clock.restore()
this.server.restore()
// if we don't close it after each test, subsequent tests get messed up.
if (helpDialog.$dialog != null) {
helpDialog.$dialog.dialog('close')
helpDialog.$dialog = null
}
helpDialog.dialogInited = false
helpDialog.teacherFeedbackInited = false
$('.ui-dialog').remove()
$('[id^=ui-id-]').remove()
$('#help-dialog').remove()
$('#fixtures').empty()
},
})
test('init', () => {
const $tester = $('<a class="help_dialog_trigger" />').appendTo('#fixtures')
helpDialog.initTriggers()
$tester.click()
ok($('.ui-dialog-content').is(':visible'), "help dialog appears when you click 'help' link")
equal($('.ui-dialog-title:contains("Links")').length, 1)
$tester.remove()
})
test('teacher feedback', function () {
helpDialog.open()
this.server.respond()
helpDialog.switchTo('#teacher_feedback')
ok(helpDialog.$dialog.find('#teacher-feedback-body').is(':visible'), 'textarea shows up')
})
// unskip in FOO-4344
QUnit.skip('focus management', function () {
helpDialog.open()
this.server.respond()
this.clock.tick(1)
helpDialog.switchTo('#create_ticket')
this.clock.tick(1)
equal(document.activeElement, helpDialog.$dialog.find('#error_subject')[0], 'focuses first input')
ok(
!helpDialog.$dialog.find('#help-dialog-options').is(':visible'),
'out of view screen is hidden'
)
helpDialog.switchTo('#help-dialog-options')
ok(helpDialog.$dialog.find('#help-dialog-options').is(':visible'), 'menu screen appears again')
})

View File

@ -1,211 +0,0 @@
//
// Copyright (C) 2011 - 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/>.
// also requires
// jquery.formSubmit
// jqueryui dialog
// jquery disableWhileLoading
import {useScope as useI18nScope} from '@canvas/i18n'
import helpDialogTemplate from './jst/helpDialog.handlebars'
import $ from 'jquery'
import {find} from 'lodash'
import htmlEscape, {raw} from '@instructure/html-escape'
import '@canvas/jquery/jquery.instructure_misc_helpers'
import '@canvas/jquery/jquery.instructure_forms'
import 'jqueryui/dialog'
import '@canvas/jquery/jquery.disableWhileLoading'
const I18n = useI18nScope('helpDialog')
const helpDialog = {
defaultTitle: I18n.t('Help', 'Help'),
showEmail: () => !ENV.current_user_id,
animateDuration: 100,
initDialog() {
helpDialog.defaultTitle = ENV.help_link_name || helpDialog.defaultTitle
helpDialog.$dialog = $('<div style="padding:0; overflow: visible;" />').dialog({
resizable: false,
width: 400,
title: helpDialog.defaultTitle,
close: () => helpDialog.switchTo('#help-dialog-options'),
modal: true,
zIndex: 1000,
})
helpDialog.$dialog
.dialog('widget')
.on(
'click',
'a[href="#teacher_feedback"],a[href="#create_ticket"],a[href="#help-dialog-options"]',
event => {
if (event) event.preventDefault()
helpDialog.switchTo($(event.currentTarget).attr('href'))
}
)
helpDialog.helpLinksDfd = $.getJSON('/help_links').done(links => {
// only show the links that are available to the roles of this user
links = $.grep(links, link =>
find(
link.available_to,
role =>
role === 'user' || (ENV.current_user_roles && ENV.current_user_roles.includes(role))
)
)
const locals = {
showEmail: helpDialog.showEmail(),
helpLinks: links,
url: window.location,
contextAssetString: ENV.context_asset_string,
userRoles: ENV.current_user_roles,
}
helpDialog.$dialog.html(helpDialogTemplate(locals))
helpDialog.initTicketForm()
// recenter the dialog once all the links have been loaded so it is back in the
// middle of the page
if (helpDialog.$dialog) helpDialog.$dialog.dialog('option', 'position', 'center')
$(this).trigger('ready')
})
helpDialog.$dialog.disableWhileLoading(helpDialog.helpLinksDfd)
helpDialog.dialogInited = true
},
initTicketForm() {
const required = ['error[subject]', 'error[comments]', 'error[user_perceived_severity]']
if (helpDialog.showEmail()) required.push('error[email]')
const $form = helpDialog.$dialog.find('#create_ticket').formSubmit({
disableWhileLoading: true,
required,
success: () => {
helpDialog.$dialog.dialog('close')
$form.find(':input').val('')
},
})
},
switchTo(panelId) {
let newTitle
const toggleablePanels = '#teacher_feedback, #create_ticket'
const homePanel = '#help-dialog-options'
helpDialog.$dialog.find(toggleablePanels).hide()
const newPanel = helpDialog.$dialog.find(panelId)
const newHeight = newPanel.show().outerHeight()
helpDialog.$dialog.animate(
{left: toggleablePanels.match(panelId) ? -400 : 0, height: newHeight},
{
step: () => {
// reposition vertically to reflect current height
if (
!(
helpDialog.dialogInited &&
helpDialog.$dialog &&
helpDialog.$dialog.hasClass('ui-dialog-content')
)
) {
helpDialog.initDialog()
}
helpDialog.$dialog && helpDialog.$dialog.dialog('option', 'position', 'center')
},
duration: helpDialog.animateDuration,
complete() {
let toFocus = newPanel.find(':input').not(':disabled')
if (!toFocus.length) toFocus = newPanel.find(':focusable')
toFocus.first().focus()
if (panelId !== homePanel) $(homePanel).hide()
},
}
)
if ((newTitle = helpDialog.$dialog.find(`a[href='${panelId}'] .text`).text())) {
newTitle = $(
`<a class='ui-dialog-header-backlink' href='#help-dialog-options'>
${htmlEscape(I18n.t('Back', 'Back'))} \
</a>
<span>
${htmlEscape(newTitle)}
</span>`
)
} else {
newTitle = helpDialog.defaultTitle
}
helpDialog.$dialog.dialog('option', 'title', newTitle)
},
open() {
if (
!(
helpDialog.dialogInited &&
helpDialog.$dialog &&
helpDialog.$dialog.hasClass('ui-dialog-content')
)
) {
helpDialog.initDialog()
}
helpDialog.$dialog.dialog('open')
helpDialog.initTeacherFeedback()
},
initTeacherFeedback() {
const currentUserIsStudent =
ENV.current_user_roles && ENV.current_user_roles.includes('student')
if (!helpDialog.teacherFeedbackInited && currentUserIsStudent) {
helpDialog.teacherFeedbackInited = true
const coursesDfd = $.getJSON('/api/v1/courses.json')
let $form
helpDialog.helpLinksDfd.done(() => {
$form = helpDialog.$dialog
.find('#teacher_feedback')
.disableWhileLoading(coursesDfd)
.formSubmit({
disableWhileLoading: true,
required: ['recipients[]', 'body'],
success: () => helpDialog.$dialog.dialog('close'),
})
})
$.when(coursesDfd, helpDialog.helpLinksDfd).done(([courses]) => {
const optionsHtml = $.map(
courses,
c =>
`<option
value='course_${c.id}_admins'
${raw(ENV.context_id === c.id ? 'selected' : '')}
>
${htmlEscape(c.name)}
</option>`
).join('')
$form.find('[name="recipients[]"]').html(optionsHtml)
})
}
},
initTriggers() {
$('.help_dialog_trigger').click(event => {
event.preventDefault()
helpDialog.open()
})
},
}
export default helpDialog

View File

@ -1,64 +0,0 @@
<div id="help-dialog">
<ul id="help-dialog-options" class="help-dialog-pane">
{{#each helpLinks}}
<li>
<a href="{{#if url}}{{url}}{{else}}#{{/if}}" target="_blank">
<span class="text">{{text}}</span>
<span class="subtext">{{subtext}}</span>
</a>
</li>
{{/each}}
</ul>
<form class="help-dialog-pane" id="teacher_feedback" style="display:none" action="/api/v1/conversations" method="POST">
<label for="teacher-feedback-recipients">
{{#t "which_course_is_this_question_about"}}Which course is this question about?{{/t}}
<small>{{#t "message_will_be_sent_to_all_the_teachers_tas_in_the_course"}}Message will be sent to all the Teachers / TA's in the course.{{/t}}</small>
</label>
<select class="input-block-level" name="recipients[]" id="teacher-feedback-recipients"></select>
<label for="teacher-feedback-body">{{#t "message"}}Message{{/t}}</label>
<textarea id="teacher-feedback-body" name="body"></textarea>
<div class="button-container">
<button type="submit" class="btn" data-text-while-loading="{{#t "sending"}}Sending...{{/t}}">{{#t "send_message"}}Send Message{{/t}}</button>
</div>
<input class="input-block-level" type="hidden" name="group_conversation" value="true">
</form>
<form class="help-dialog-pane bootstrap-form" id="create_ticket" style="display:none" action="/error_reports" method="POST">
<h4>{{#t "file_a_ticket_for_a_personal_response_from_our_support_team"}}File a ticket for a personal response from our support team{{/t}}</h4>
<div role="alert" class="alert">
<strong>{{#t "for_an_instant_answer"}}For an instant answer:{{/t}}</strong>
<div>{{#t "see_if_your_issue_is_addressed_in_the_canvas_guides"}}See if your issue is addressed in the <a target="_blank" href="http://guides.canvaslms.com/">Canvas Guides</a>.{{/t}}</div>
</div>
<div>
<label for="error_subject">{{#t "subject"}}Subject{{/t}}</label>
<input type="text" class="input-block-level" id="error_subject" name="error[subject]" />
</div>
<div>
<label for="error-comments">
{{#t "description"}}Description{{/t}}
<small>{{#t "include_a_link_to_a_screencast_or_screenshot_using_something_like_jing"}}Include a link to a screencast/screenshot using something like <a target="_blank" href="http://www.techsmith.com/download/jing">Jing</a>.{{/t}}</small>
</label>
<textarea class="input-block-level" id="error-comments" name="error[comments]"></textarea>
</div>
<label for="severity">{{#t "how_is_this_affecting_you"}}How is this affecting you?{{/t}}</label>
<select class="input-block-level" name="error[user_perceived_severity]" id="severity">
<option value="">{{#t "please_select_one"}}Please select one...{{/t}}</option>
<option value="just_a_comment">{{#t "just_a_casual_question_comment_idea_suggestion"}}Just a casual question, comment, idea, suggestion{{/t}}</option>
<option value="not_urgent">{{#t "i_need_some_help_but_its_not_urgent"}}I need some help but it's not urgent{{/t}}</option>
<option value="workaround_possible">{{#t "somethings_broken_but_i_can_work_around_it_for_now"}}Something's broken but I can work around it for now{{/t}}</option>
<option value="blocks_what_i_need_to_do">{{#t "i_cant_get_things_done_until_i_hear_back_from_you"}}I can't get things done until I hear back from you{{/t}}</option>
<option value="extreme_critical_emergency">{{#t "extreme_critical_emergency"}}EXTREME CRITICAL EMERGENCY!!{{/t}}</option>
</select>
<div style="{{hiddenUnless showEmail}}">
<label for="error-email">{{#t "your_email_address"}}Your email address{{/t}}</label>
<input class="input-block-level" type="email" name="error[email]" id="error-email">
</div>
<input class="input-block-level" type="hidden" name="error[url]" value="{{url}}">
<input class="input-block-level" type="hidden" name="error[context_asset_string]" value="{{contextAssetString}}">
<input class="input-block-level" type="hidden" name="error[user_roles]" value="{{userRoles}}">
{{! this is a honeypot field. it's hidden via css, but spambots don't know that. }}
<input class="input-block-level hidden" name="error[username]" value="">
<div class="button-container">
<button type="submit" data-text-while-loading="{{#t "Submitting_Ticket"}}Submitting Ticket...{{/t}}" class="btn submit_button"><img src="/images/email.png" alt=""/>{{#t "submit_this_support_request"}}Submit Ticket{{/t}}</button>
</div>
</form>
</div>

View File

@ -1,3 +0,0 @@
{
"i18nScope": "help_dialog"
}

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,20 @@
/*
* Copyright (C) 2024 - 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/>.
*/
export {default} from './react/index'
export {renderLoginHelp} from './react/loginHelp'

View File

@ -0,0 +1,7 @@
{
"name": "@canvas/help-dialog",
"private": true,
"version": "1.0.0",
"author": "Instructure",
"main": "index.tsx"
}

View File

@ -23,8 +23,8 @@ import {Heading} from '@instructure/ui-heading'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
// @ts-expect-error
import PandaMapSVGURL from '../../images/panda-map.svg'
import type {HelpLink} from '../../../../api.d'
import PandaMapSVGURL from '../images/panda-map.svg'
import type {HelpLink} from '../../../api.d'
type Props = {
featuredLink: HelpLink

View File

@ -30,7 +30,7 @@ import {ScreenReaderContent, PresentationContent} from '@instructure/ui-a11y-con
import tourPubSub from '@canvas/tour-pubsub'
import {useQuery} from '@canvas/query'
import helpLinksQuery from '../queries/helpLinksQuery'
import type {HelpLink} from '../../../../api.d'
import type {HelpLink} from '../../../api.d'
const I18n = useI18nScope('HelpLinks')

View File

@ -0,0 +1,91 @@
/**
* Canvas LMS - The open-source learning management system
*
* Copyright (C) 2024 Instructure, Inc.
*
* This program 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.
*
* This program 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 <https://www.gnu.org/licenses/>.Wh
*/
import {useScope as useI18nScope} from '@canvas/i18n'
import React, {useState} from 'react'
import ReactDOM from 'react-dom'
import {QueryProvider} from '@canvas/query'
import HelpDialog from '.'
import {Modal} from '@instructure/ui-modal'
import {Link} from '@instructure/ui-link'
import {Heading} from '@instructure/ui-heading'
import {CloseButton} from '@instructure/ui-buttons'
const I18n = useI18nScope('HelpLinks')
interface LoginHelpProps {
linkText: string
}
const modalLabel = () => I18n.t('Login Help for %{canvas}', {canvas: 'Canvas LMS'})
const LoginHelp = ({linkText}: LoginHelpProps): JSX.Element => {
// Initial modal state is open, because this whole thing initially
// loads in response to to the user clicking on the bare "help" link.
const [open, setOpen] = useState(true)
function openHelpModal(): void {
setOpen(true)
}
function closeHelpModal(): void {
setOpen(false)
}
return (
<>
<Link href="#" onClick={openHelpModal}>
{linkText}
</Link>
<Modal size="small" label={modalLabel()} open={open} onDismiss={closeHelpModal}>
<Modal.Header>
<CloseButton
data-testid="login-help-close-button"
placement="end"
offset="medium"
onClick={closeHelpModal}
screenReaderLabel={I18n.t('Close help dialog')}
/>
<Heading level="h3" as="h2">
{modalLabel()}
</Heading>
</Modal.Header>
<Modal.Body>
<HelpDialog onFormSubmit={closeHelpModal} />
</Modal.Body>
</Modal>
</>
)
}
export function renderLoginHelp(loginLink: Element): void {
// wrap the help link in a span we can hang the modal off of.
// then render the React modal into it. Be sure we're actually
// getting an anchor element.
if (loginLink.tagName !== 'A') throw new TypeError('loginLink must be an <a> element')
const linkText = loginLink.textContent ?? ''
const wrapper = document.createElement('span')
loginLink.replaceWith(wrapper)
wrapper.appendChild(loginLink)
ReactDOM.render(
<QueryProvider>
<LoginHelp linkText={linkText} />
</QueryProvider>,
wrapper
)
}