Refactor in preparation for publishing canvas-media on npm

closes MAT-108
flag=none

Primary changes revolved around getting canvas-media to talk to RCS
instead of directly to canvas. This necessitated passing more
info from canvas -> rce -> canvas-media so it can connect.

In the process, merged the 2 functions that uploaded closed captions
into one.

the change in root.js that lazy loads tinyRCE is to get the
canvas-rce bundle size below size limits, which it blew
after a rebase.

test plan:
  This should work with you config/dynamic_settings.yml
  development.config.canvas.rich-content-service.app-host
  value = 'http://host:port'
  or simply 'host:port'

  - in the RCE, upload a video with closed caption
    using the Media > Upload/Record command.
  > expect the video to show up with CC
  - edit the captions in the video options tray
  > expect the captions to be updated
  > bonus result: no console warning about missing file.name
    prop from ClosedCaptionPanel

  - in the RCE, open Upload Document and select
    a video.
  > expect the video to upload an show up.

  - create a media recording type assignment
  - enable the "Assignment Enhancements - Student" feature
  - as a student, visit the assignment
  - click the "Recort/Upload" button
  > expect the video upload and CC feature to work as expected
  > bonus fix: the CCs should be at the bottom of the video

Change-Id: I7b574bb67998072324954a6b481e0c4d3b3251de
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/264109
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jeff Largent <jeff.largent@instructure.com>
QA-Review: Jeff Largent <jeff.largent@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2021-05-03 16:03:01 -04:00
parent bae404fc49
commit 044846ef96
21 changed files with 407 additions and 654 deletions

View File

@ -42,6 +42,7 @@
"@instructure/ui-media-player": "^7.2",
"@instructure/ui-modal": "^7",
"@instructure/ui-pagination": "^7",
"@instructure/ui-progress": "^7",
"@instructure/ui-react-utils": "^7",
"@instructure/ui-select": "^7",
"@instructure/ui-spinner": "^7",

View File

@ -49,8 +49,12 @@ export default class UploadMedia extends React.Component {
})
),
liveRegion: func,
contextId: string,
contextType: string,
rcsConfig: shape({
contextId: string,
contextType: string,
origin: string.isRequired,
headers: shape({Authentication: string.isRequired})
}),
onStartUpload: func,
onUploadComplete: func,
onDismiss: func,
@ -115,13 +119,7 @@ export default class UploadMedia extends React.Component {
uploadFile(file) {
this.setState({uploading: true})
this.props.onStartUpload && this.props.onStartUpload(file)
saveMediaRecording(
file,
this.props.contextId,
this.props.contextType,
this.saveMediaCallback,
this.onSaveMediaProgress
)
saveMediaRecording(file, this.props.rcsConfig, this.saveMediaCallback, this.onSaveMediaProgress)
}
onSaveMediaProgress = progress => {
@ -134,7 +132,11 @@ export default class UploadMedia extends React.Component {
} else {
try {
if (this.state.selectedPanel === PANELS.COMPUTER && this.state.subtitles.length > 0) {
await saveClosedCaptions(data.mediaObject.media_object.media_id, this.state.subtitles)
await saveClosedCaptions(
data.mediaObject.media_object.media_id,
this.state.subtitles,
this.props.rcsConfig
)
}
this.props.onUploadComplete && this.props.onUploadComplete(null, data)
} catch (ex) {

View File

@ -33,15 +33,20 @@ const uploadMediaTranslations = {
SUBMIT_TEXT: 'Submit',
UPLOADING_ERROR: 'Upload Error',
UPLOAD_MEDIA_LABEL: 'Upload Media',
MEDIA_RECORD_NOT_AVAILABLE: 'Record not available'
MEDIA_RECORD_NOT_AVAILABLE: 'Record not available',
PROGRESS_LABEL: 'Making progress'
}
}
function renderComponent(overrideProps = {}) {
return render(
<UploadMedia
contextType="course"
contextId="17"
rcsConfig={{
contextType: 'course',
contextId: '17',
origin: 'http://host:port',
jwt: 'whocares'
}}
open
liveRegion={() => null}
onStartUpload={() => {}}
@ -79,13 +84,10 @@ describe('Upload Media', () => {
it('is enabled once ComputerPanel has a file', () => {
const {getByText} = renderComponent({
tabs: {upload: true},
computerFile: {
lastModified: 1568991600840,
lastModifiedDate: new Date(1568991600840),
name: 'dummy-video.mp4',
size: 1875112,
computerFile: new File(['bits'], 'dummy-video.mp4', {
lastModifiedDate: 1568991600840,
type: 'video/mp4'
}
})
})
expect(getByText('Submit').closest('button')).not.toHaveAttribute('disabled')
})
@ -99,13 +101,10 @@ describe('Upload Media', () => {
const {getByText} = renderComponent({
onStartUpload,
tabs: {upload: true},
computerFile: {
lastModified: 1568991600840,
lastModifiedDate: new Date(1568991600840),
name: 'dummy-video.mp4',
size: 1875112,
computerFile: new File(['bits'], 'dummy-video.mp4', {
lastModifiedDate: 1568991600840,
type: 'video/mp4'
}
})
})
fireEvent.click(getByText('Submit'))

View File

@ -37,6 +37,13 @@ function mediaServerSession() {
}
describe('saveMediaRecording', () => {
const rcsConfig = {
contentId: '1',
contentType: 'course',
origin: 'http://host:port',
jwt: 'doesnotmatter'
}
beforeEach(() => {
moxios.install()
})
@ -44,26 +51,26 @@ describe('saveMediaRecording', () => {
moxios.uninstall()
})
it('fails if request for kaltura session fails', async done => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 500,
response: {error: 'womp womp'}
})
const doneFunction = jest.fn()
sinon.stub(K5Uploader.prototype, 'loadUiConf').callsFake(() => 'mock')
await saveMediaRecording({}, '1', 'course', doneFunction)
await saveMediaRecording({}, rcsConfig, doneFunction)
expect(doneFunction).toHaveBeenCalledTimes(1)
expect(doneFunction.mock.calls[0][0].message).toBe('Request failed with status code 500')
done()
})
it('returns error if k5.fileError is dispatched', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
const doneFunction = jest.fn()
const progressFunction = jest.fn()
return saveMediaRecording({}, '1', 'course', doneFunction, progressFunction).then(uploader => {
return saveMediaRecording({}, rcsConfig, doneFunction, progressFunction).then(uploader => {
uploader.dispatchEvent('K5.fileError', {error: 'womp womp'}, uploader)
expect(doneFunction).toHaveBeenCalledTimes(1)
expect(doneFunction.mock.calls[0][0].error).toBe('womp womp')
@ -71,14 +78,14 @@ describe('saveMediaRecording', () => {
})
it('k5.ready calls uploaders uploadFile with file', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
const doneFunction = jest.fn()
const progressFunction = jest.fn()
const uploadFileFunc = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction, progressFunction).then(
return saveMediaRecording({file: 'thing'}, rcsConfig, doneFunction, progressFunction).then(
uploader => {
uploader.uploadFile = uploadFileFunc
uploader.dispatchEvent('K5.ready', uploader)
@ -89,14 +96,14 @@ describe('saveMediaRecording', () => {
})
it('k5.progress calls progress function when dispatched', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
const doneFunction = jest.fn()
const progressFunction = jest.fn()
const uploadFileFunc = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction, progressFunction).then(
return saveMediaRecording({file: 'thing'}, rcsConfig, doneFunction, progressFunction).then(
uploader => {
uploader.uploadFile = uploadFileFunc
uploader.dispatchEvent('K5.progress', uploader)
@ -106,7 +113,7 @@ describe('saveMediaRecording', () => {
})
it('k5.complete calls done with canvasMediaObject data if succeeds', () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
@ -116,7 +123,7 @@ describe('saveMediaRecording', () => {
})
const doneFunction2 = jest.fn()
const progressFunction = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2, progressFunction).then(
return saveMediaRecording({file: 'thing'}, rcsConfig, doneFunction2, progressFunction).then(
async uploader => {
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
await new Promise(setTimeout)
@ -131,7 +138,7 @@ describe('saveMediaRecording', () => {
})
it('fails if request to create media object fails', async () => {
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
moxios.stubRequest('http://host:port/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: mediaServerSession()
})
@ -141,7 +148,7 @@ describe('saveMediaRecording', () => {
})
const doneFunction2 = jest.fn()
const progressFunction = jest.fn()
return saveMediaRecording({file: 'thing'}, '1', 'course', doneFunction2, progressFunction).then(
return saveMediaRecording({file: 'thing'}, rcsConfig, doneFunction2, progressFunction).then(
async uploader => {
uploader.dispatchEvent('K5.complete', {stuff: 'datatatatatatatat'}, uploader)
await new Promise(setTimeout)
@ -153,13 +160,19 @@ describe('saveMediaRecording', () => {
})
describe('saveClosedCaptions', () => {
const rcsConfig = {
host: 'host:port',
jwt: 'doesnotmatter',
method: 'PUT'
}
beforeEach(() => {
moxios.install()
})
afterEach(() => {
moxios.uninstall()
})
it('returns success promise if axios requests returns correctly', done => {
it('returns success promise if axios requests returns correctly', () => {
const mediaId = '4'
const fileContents = 'file contents'
const file = new Blob([fileContents], {type: 'text/plain'})
@ -167,23 +180,15 @@ describe('saveClosedCaptions', () => {
language: {selectedOptionId: 'en'},
file
}
moxios.stubRequest(`/media_objects/${mediaId}/media_tracks`, {
moxios.stubRequest(`${rcsConfig.origin}/api/media_objects/${mediaId}/media_tracks`, {
status: 200,
response: {data: 'media object data'}
})
const successPromise = saveClosedCaptions(mediaId, [fileAndLanguage])
return successPromise
.then(() => {
expect(true).toBe(true)
done()
})
.catch(() => {
expect(false).toBe(true)
done()
})
const successPromise = saveClosedCaptions(mediaId, [fileAndLanguage], rcsConfig)
return expect(successPromise).resolves.toMatchObject({data: {data: 'media object data'}})
})
it('returns failure promise if axios request fails', done => {
it('returns failure promise if axios request fails', () => {
const mediaId = '4'
const fileContents = 'file contents'
const file = new Blob([fileContents], {type: 'text/plain'})
@ -191,19 +196,11 @@ describe('saveClosedCaptions', () => {
language: {selectedOptionId: 'en'},
file
}
moxios.stubRequest(`/media_objects/${mediaId}/media_tracks`, {
moxios.stubRequest(`${rcsConfig.origin}/api/media_objects/${mediaId}/media_tracks`, {
status: 500,
response: {data: 'media object data'}
})
const successPromise = saveClosedCaptions(mediaId, [fileAndLanguage])
return successPromise
.then(() => {
expect(false).toBe(true)
done()
})
.catch(() => {
expect(true).toBe(true)
done()
})
const successPromise = saveClosedCaptions(mediaId, [fileAndLanguage], rcsConfig)
return expect(successPromise).rejects.toMatchObject({response: {status: 500}})
})
})

View File

@ -22,7 +22,7 @@ import RocketSVG from './RocketSVG'
import useComputerPanelFocus from './useComputerPanelFocus'
import {isAudio, isVideo, isPreviewable, sizeMediaPlayer} from './shared/utils'
import LoadingIndicator from './shared/LoadingIndicator'
import saveMediaRecording from './saveMediaRecording'
import saveMediaRecording, {saveClosedCaptions} from './saveMediaRecording'
export {
UploadMedia as default,
@ -34,5 +34,6 @@ export {
isPreviewable,
sizeMediaPlayer,
LoadingIndicator,
saveMediaRecording
saveMediaRecording,
saveClosedCaptions
}

View File

@ -95,12 +95,15 @@ function addUploaderFileCompleteEventListeners(uploader, context, file, done, on
})
}
export default async function saveMediaRecording(file, contextId, contextType, done, onProgress) {
export default async function saveMediaRecording(file, rcsConfig, done, onProgress) {
try {
window.addEventListener('beforeunload', handleUnloadWhileUploading)
const mediaServerSession = await axios.post(
'/api/v1/services/kaltura_session?include_upload_config=1'
)
const mediaServerSession = await axios({
method: 'POST',
url: `${rcsConfig.origin}/api/v1/services/kaltura_session?include_upload_config=1`,
headers: rcsConfig.headers
})
if (onProgress) {
onProgress(STARTING_PROGRESS_VALUE)
}
@ -114,38 +117,60 @@ export default async function saveMediaRecording(file, contextId, contextType, d
addUploaderProgressEventListeners(k5UploaderSession, onProgress)
}
addUploaderFileErrorEventListeners(k5UploaderSession, done, file)
addUploaderFileCompleteEventListeners(
k5UploaderSession,
{contextId, contextType},
file,
done,
onProgress
)
addUploaderFileCompleteEventListeners(k5UploaderSession, rcsConfig, file, done, onProgress)
return k5UploaderSession
} catch (err) {
doDone(done, err, {uploadedFile: file})
}
}
export async function saveClosedCaptions(mediaId, files) {
const axiosRequests = []
files.forEach(function (file) {
const p = new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (e) {
const content = e.target.result
const params = {
content,
locale: file.locale,
'exclude[]': 'sources'
/*
* @media_object_id: id of the media_object we're assigning CC to
* @subtitles: [{locale: string locale, file: JS File object}]
* @rcsConfig: {origin, headers, method} where method=PUT for update or POST for create
*/
export async function saveClosedCaptions(media_object_id, subtitles, rcsConfig) {
// read all the subtitle files' contents
const file_promises = []
subtitles.forEach(st => {
if (st.isNew) {
const p = new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (e) {
resolve({locale: st.locale, content: e.target.result})
}
axios.post(`/media_objects/${mediaId}/media_tracks`, params).then(resolve).catch(reject)
}
reader.readAsText(file.file)
})
axiosRequests.push(p)
reader.onerror = function (e) {
e.target.abort()
reject(e.target.error || e)
}
reader.readAsText(st.file)
})
file_promises.push(p)
} else {
file_promises.push(Promise.resolve({locale: st.locale}))
}
})
return Promise.all(axiosRequests)
// once all the promises from reading the subtitles' files
// have resolved, PUT/POST the resulting subtitle objects to the RCS
// when that completes, the update_promise will resolve
const update_promise = new Promise((resolve, reject) => {
Promise.all(file_promises)
.then(closed_captions => {
axios({
method: rcsConfig.method || 'PUT',
url: `${rcsConfig.origin}/api/media_objects/${media_object_id}/media_tracks`,
headers: rcsConfig.headers,
data: closed_captions
})
.then(resolve)
.catch(e => {
reject(e)
})
})
.catch(e => reject(e))
})
return update_promise
}
function doDone(done, ...rest) {

View File

@ -19,6 +19,7 @@
import normalizeLocale from './rce/normalizeLocale'
import {renderIntoDiv as render} from './rce/root'
import getRceTranslations from './getRceTranslations'
import {headerFor, originFromHost} from './sidebar/sources/api'
import 'tinymce'
if (process.env.BUILD_LOCALE && process.env.BUILD_LOCALE !== 'en') {
@ -55,3 +56,11 @@ export function renderIntoDiv(editorEl, props, cb) {
})
}
}
export function getRCSAuthenticationHeaders(jwt) {
return headerFor(jwt)
}
export function getRCSOriginFromHost(host) {
return originFromHost(host)
}

View File

@ -1,42 +0,0 @@
/*
* 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 formatMessage from '../../../../format-message'
const MediaCaptureStrings = {
ARIA_VIDEO_LABEL: formatMessage('Video Player'),
ARIA_VOLUME: formatMessage('Current Volume Level'),
ARIA_RECORDING: formatMessage('Recording'),
DEFAULT_ERROR: formatMessage('Something went wrong accessing your mic or webcam.'),
DEVICE_AUDIO: formatMessage('Mic'),
DEVICE_VIDEO: formatMessage('Webcam'),
FILE_PLACEHOLDER: formatMessage('Untitled'),
FINISH: formatMessage('Finish'),
NO_WEBCAM: formatMessage('No Video'),
NOT_ALLOWED_ERROR: formatMessage('Please allow Canvas to access your microphone and webcam.'),
NOT_READABLE_ERROR: formatMessage('Your webcam may already be in use.'),
PLAYBACK_PAUSE: formatMessage('Pause'),
PLAYBACK_PLAY: formatMessage('Play'),
PREVIEW: formatMessage('PREVIEW'),
SAVE: formatMessage('Save'),
SR_FILE_INPUT: formatMessage('File name'),
START: formatMessage('Start Recording'),
START_OVER: formatMessage('Start Over')
}
export {MediaCaptureStrings}

View File

@ -1,50 +0,0 @@
/*
* 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 {Alert} from '@instructure/ui-alerts'
import {canUseMediaCapture, MediaCapture} from '@instructure/media-capture'
import formatMessage from '../../../../format-message'
import {MediaCaptureStrings} from './MediaCaptureStrings'
import {object, func} from 'prop-types'
import React from 'react'
export default class MediaRecorder extends React.Component {
saveFile = file => {
this.props.contentProps.saveMediaRecording(file, this.props.editor, this.props.dismiss)
}
render() {
return (
<div>
{canUseMediaCapture() ? (
<MediaCapture translations={MediaCaptureStrings} onCompleted={this.saveFile} />
) : (
<Alert variant="error" margin="small">
{formatMessage('Error uploading video/audio recording')}
</Alert>
)}
</div>
)
}
}
MediaRecorder.propTypes = {
contentProps: object.isRequired,
dismiss: func.isRequired,
editor: object.isRequired
}

View File

@ -1,32 +0,0 @@
/*
* 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 {UploadMedia} from '../index'
describe('UploadMedia', () => {
it('calls onDismiss prop when closing', () => {
const handleDismiss = jest.fn()
const {getAllByText} = render(<UploadMedia editor={{}} onDismiss={handleDismiss} />)
const closeBtn = getAllByText('Close')[0]
fireEvent.click(closeBtn)
expect(handleDismiss).toHaveBeenCalled()
})
})

View File

@ -1,203 +0,0 @@
/*
* 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 {Alert} from '@instructure/ui-alerts'
import {Button, CloseButton} from '@instructure/ui-buttons'
import formatMessage from '../../../../format-message'
import {func, object} from 'prop-types'
import {Heading} from '@instructure/ui-heading'
import {Spinner} from '@instructure/ui-spinner'
import {Modal} from '@instructure/ui-modal'
import {Mask} from '@instructure/ui-overlays'
import React, {Suspense, useState} from 'react'
import {Tabs} from '@instructure/ui-tabs'
import {View} from '@instructure/ui-view'
import Bridge from '../../../../bridge'
import {StoreProvider} from '../../shared/StoreContext'
const MediaRecorder = React.lazy(() => import('./MediaRecorder'))
const ComputerPanel = React.lazy(() => import('../../shared/Upload/ComputerPanel'))
export const PANELS = {
COMPUTER: 0,
RECORD: 1
}
const ALERT_TIMEOUT = 5000
const ACCEPTED_FILE_TYPES = [
'3gp',
'aac',
'amr',
'asf',
'avi',
'flac',
'flv',
'm4a',
'm4v',
'mkv',
'mov',
'mp3',
'mp4',
'mpeg',
'mpg',
'ogg',
'qt',
'wav',
'wma',
'wmv'
]
export const handleSubmit = (editor, selectedPanel, uploadData, saveMediaRecording, onDismiss) => {
const {theFile} = uploadData
saveMediaRecording('media', theFile)
onDismiss()
}
function renderLoading() {
return formatMessage('Loading')
}
function renderLoadingMedia() {
return formatMessage('Loading media')
}
function loadingErrorSuccess(states) {
if (states.uploadingMediaStatus.loading) {
return (
<Mask>
<Spinner renderTitle={renderLoading} size="large" margin="0 0 0 medium" />
</Mask>
)
} else if (states.uploadingMediaStatus.error) {
return (
<Alert variant="error" margin="small" timeout={ALERT_TIMEOUT}>
{formatMessage('Error uploading video/audio recording')}
</Alert>
)
} else if (states.uploadingMediaStatus.uploaded) {
return <Alert timeout={ALERT_TIMEOUT}>{formatMessage('Video/audio recording uploaded')}</Alert>
}
}
export function UploadMedia(props) {
const [theFile, setFile] = useState(null)
const [hasUploadedFile, setHasUploadedFile] = useState(false)
const [selectedPanel, setSelectedPanel] = useState(0)
const trayProps = Bridge.trayProps.get(props.editor)
return (
<StoreProvider {...trayProps}>
{contentProps => (
<Modal
label={formatMessage('Upload Media')}
size="medium"
onDismiss={props.onDismiss}
open
shouldCloseOnDocumentClick={false}
>
<Modal.Header>
<CloseButton onClick={props.onDismiss} offset="medium" placement="end">
{formatMessage('Close')}
</CloseButton>
<Heading>{formatMessage('Upload Media')}</Heading>
</Modal.Header>
<Modal.Body>
{loadingErrorSuccess(contentProps.upload)}
<Tabs
shouldFocusOnRender
size="large"
selectedIndex={selectedPanel}
onRequestTabChange={newIndex => setSelectedPanel(newIndex)}
>
<Tabs.Panel title={formatMessage('Computer')}>
<Suspense
fallback={
<View as="div" height="100%" width="100%" textAlign="center">
<Spinner
renderTitle={renderLoadingMedia}
size="large"
margin="0 0 0 medium"
/>
</View>
}
size="large"
>
<ComputerPanel
editor={props.editor}
theFile={theFile}
setFile={setFile}
hasUploadedFile={hasUploadedFile}
setHasUploadedFile={setHasUploadedFile}
label={formatMessage('Drag a File Here')}
accept={ACCEPTED_FILE_TYPES}
/>
</Suspense>
</Tabs.Panel>
<Tabs.Panel title={formatMessage('Record')}>
<Suspense
fallback={
<View as="div" height="100%" width="100%" textAlign="center">
<Spinner
renderTitle={renderLoadingMedia}
size="large"
margin="0 0 0 medium"
/>
</View>
}
>
<MediaRecorder
editor={props.editor}
dismiss={props.onDismiss}
contentProps={contentProps}
/>
</Suspense>
</Tabs.Panel>
</Tabs>
</Modal.Body>
{selectedPanel !== PANELS.RECORD && (
<Modal.Footer>
<Button onClick={props.onDismiss}>{formatMessage('Close')}</Button>&nbsp;
<Button
onClick={e => {
e.preventDefault()
handleSubmit(
props.editor,
selectedPanel,
{theFile},
contentProps.saveMediaRecording,
props.onDismiss
)
}}
variant="primary"
type="submit"
>
{formatMessage('Submit')}
</Button>
</Modal.Footer>
)}
</Modal>
)}
</StoreProvider>
)
}
UploadMedia.propTypes = {
onDismiss: func.isRequired,
editor: object.isRequired
}

View File

@ -226,7 +226,7 @@ export default function VideoOptionsTray(props) {
<ClosedCaptionPanel
subtitles={subtitles.map(st => ({
locale: st.locale,
file: {name: st.language} // this is an artifact of ClosedCaptionCreatorRow's inards
file: {name: st.language || st.locale} // this is an artifact of ClosedCaptionCreatorRow's inards
}))}
uploadMediaTranslations={Bridge.uploadMediaTranslations}
languages={Bridge.languages}

View File

@ -21,11 +21,11 @@ import ReactDOM from 'react-dom'
import Bridge from '../../../bridge'
import {StoreProvider} from '../shared/StoreContext'
import formatMessage from '../../../format-message'
import {headerFor, originFromHost} from '../../../sidebar/sources/api'
export default function(ed, document) {
export default function (ed, document) {
return import('@instructure/canvas-media').then(CanvasMedia => {
const UploadMedia = CanvasMedia.default
// return import('./UploadMedia').then(({UploadMedia}) => {
let container = document.querySelector('.canvas-rce-media-upload')
if (!container) {
container = document.createElement('div')
@ -80,8 +80,12 @@ export default function(ed, document) {
<StoreProvider {...trayProps}>
{contentProps => (
<UploadMedia
contextType={ed.settings.canvas_rce_user_context.type}
contextId={ed.settings.canvas_rce_user_context.id}
rcsConfig={{
contextType: ed.settings.canvas_rce_user_context.type,
contextId: ed.settings.canvas_rce_user_context.id,
origin: originFromHost(contentProps.host),
headers: headerFor(contentProps.jwt)
}}
languages={Bridge.languages}
open
liveRegion={() => document.getElementById('flash_screenreader_holder')}

View File

@ -19,7 +19,6 @@
import React, {createRef} from 'react'
import {render, unmountComponentAtNode} from 'react-dom'
import RCEWrapper from './RCEWrapper'
import tinyRCE from './tinyRCE'
import normalizeProps from './normalizeProps'
import formatMessage from '../format-message'
@ -32,24 +31,28 @@ if (!process?.env?.BUILD_LOCALE) {
}
export function renderIntoDiv(target, props, renderCallback) {
// normalize props
props = normalizeProps(props, tinyRCE)
import('./tinyRCE').then(module => {
const tinyRCE = module.default
formatMessage.setup({locale: props.language})
// render the editor to the target element
const renderedComponent = createRef()
render(
<RCEWrapper
ref={renderedComponent}
{...props}
handleUnmount={() => unmountComponentAtNode(target)}
/>,
target,
() => {
// pass it back
renderCallback && renderCallback(renderedComponent.current)
}
)
// normalize props
props = normalizeProps(props, tinyRCE)
formatMessage.setup({locale: props.language})
// render the editor to the target element
const renderedComponent = createRef()
render(
<RCEWrapper
ref={renderedComponent}
{...props}
handleUnmount={() => unmountComponentAtNode(target)}
/>,
target,
() => {
// pass it back
renderCallback && renderCallback(renderedComponent.current)
}
)
})
}
// Adding this event listener fixes LA-212. I have no idea why. In Safari it

View File

@ -17,6 +17,7 @@
*/
import {saveMediaRecording} from '@instructure/canvas-media'
import {headerFor, originFromHost} from '../sources/api'
import * as files from './files'
import * as images from './images'
import bridge from '../../bridge'
@ -135,10 +136,7 @@ export function embedUploadResult(results, selectedTabType) {
const embedData = fileEmbed(results)
if (selectedTabType === 'images' && isImage(embedData.type) && results.displayAs !== 'link') {
// embed the image after any current selection rather than link to it or replace it
bridge
.activeEditor()
?.mceInstance()
?.selection.collapse()
bridge.activeEditor()?.mceInstance()?.selection.collapse()
const file_props = {
href: results.href || results.url,
title: results.title,
@ -153,10 +151,7 @@ export function embedUploadResult(results, selectedTabType) {
bridge.insertImage(file_props)
} else if (selectedTabType === 'media' && isAudioOrVideo(embedData.type)) {
// embed media after any current selection rather than link to it or replace it
bridge
.activeEditor()
?.mceInstance()
?.selection.collapse()
bridge.activeEditor()?.mceInstance()?.selection.collapse()
// when we record audio, notorious thinks it's a video. use the content type we got
// from the recoreded file, not the returned media object.
@ -222,7 +217,7 @@ export function fetchFolders(bookmark) {
// uploads handled via canvas-media
export function mediaUploadComplete(error, uploadData) {
const {mediaObject, uploadedFile} = uploadData
const {mediaObject, uploadedFile} = uploadData || {}
return (dispatch, _getState) => {
if (error) {
dispatch(failMediaUpload(error))
@ -263,8 +258,12 @@ export function uploadToMediaFolder(tabContext, fileMetaProps) {
if (tabContext === 'media' && fileMetaProps.domObject) {
return saveMediaRecording(
fileMetaProps.domObject,
contextId,
contextType,
{
contextId,
contextType,
origin: originFromHost(host),
headers: headerFor(jwt)
},
(err, uploadData) => {
dispatch(mediaUploadComplete(err, uploadData))
}

View File

@ -32,7 +32,9 @@ export function propsFromState(state) {
upload,
session,
newPageLinkExpanded,
all_files
all_files,
jwt,
host
} = state
const collections = {}
@ -57,6 +59,8 @@ export function propsFromState(state) {
session,
newPageLinkExpanded,
...ui,
all_files
all_files,
jwt,
host
}
}

View File

@ -18,14 +18,30 @@
import 'isomorphic-fetch'
import {parse} from 'url'
import {saveClosedCaptions} from '@instructure/canvas-media'
import {downloadToWrap, fixupFileUrl} from '../../common/fileUrl'
import formatMessage from '../../format-message'
import alertHandler from '../../rce/alertHandler'
function headerFor(jwt) {
export function headerFor(jwt) {
return {Authorization: 'Bearer ' + jwt}
}
export function originFromHost(host, windowOverride) {
let origin = host
if (typeof origin !== 'string') {
origin = ''
} else if (origin && origin.substr(0, 4) !== 'http') {
origin = `//${origin}`
const windowHandle = windowOverride || (typeof window !== 'undefined' ? window : undefined)
if (origin.length > 0 && windowHandle?.location?.protocol) {
origin = `${windowHandle.location.protocol}${origin}`
}
}
return origin
}
// filter a response to raise an error on a 400+ status
function checkStatus(response) {
if (response.status < 400) {
@ -200,52 +216,16 @@ class RceApiSource {
// PUT to //RCS/api/media_objects/:mediaId/media_tracks [{locale, content}, ...]
// receive back a 200 with the new subtitles, or a 4xx error
updateClosedCaptions(apiProps, {media_object_id, subtitles}) {
// read all the subtitle files' contents
const file_promises = []
subtitles.forEach(st => {
if (st.isNew) {
const p = new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function(e) {
resolve({locale: st.locale, content: e.target.result})
}
reader.onerror = function(e) {
e.target.abort()
reject(e)
}
reader.readAsText(st.file)
})
file_promises.push(p)
} else {
file_promises.push(Promise.resolve({locale: st.locale}))
}
return saveClosedCaptions(media_object_id, subtitles, {
origin: originFromHost(apiProps.host),
headers: headerFor(apiProps.jwt)
}).catch(e => {
console.error('Failed saving CC', e)
this.alertFunc({
text: formatMessage('Uploading closed captions/subtitles failed.'),
variant: 'error'
})
})
// once all the promises from reading the subtitles' files
// have resolved, PUT the resulting subtitle objects to the RCS
// when that completes, the update_promise will resolve
const update_promise = new Promise((resolve, reject) => {
Promise.all(file_promises)
.then(closed_captions => {
const uri = `${this.baseUri(
'media_objects',
apiProps.host
)}/${media_object_id}/media_tracks`
return this.apiPost(uri, headerFor(this.jwt), closed_captions, 'PUT')
.then(resolve)
.catch(e => {
console.error('failed updating media_tracks') // eslint-disable-line no-console
reject(e)
})
})
.catch(_e => {
this.alertFunc({
text: formatMessage('Reading a media track file failed. Aborting.'),
variant: 'error'
})
})
})
return update_promise
}
// GET /media_objects/:mediaId/media_tracks
@ -492,20 +472,8 @@ class RceApiSource {
if (!host && this.host) {
host = this.host
}
if (typeof host !== 'string') {
host = ''
} else if (host && host.substr(0, 4) !== 'http') {
host = `//${host}`
const windowHandle = windowOverride || (typeof window !== 'undefined' ? window : undefined)
if (
host.length > 0 &&
windowHandle &&
windowHandle.location &&
windowHandle.location.protocol
) {
host = `${windowHandle.location.protocol}${host}`
}
}
host = originFromHost(host, windowOverride)
const sharedEndpoints = ['images', 'media', 'documents', 'all'] // 'all' will eventually be something different
const endpt = sharedEndpoints.includes(endpoint) ? 'documents' : endpoint
return `${host}/api/${endpt}`

View File

@ -82,13 +82,14 @@ describe('Upload data actions', () => {
})
const defaults = {
host: 'http://host:port',
jwt: 'theJWT',
source: successSource
}
function setupState(props) {
const {jwt, source} = {...defaults, ...props}
return {jwt, source}
const {host, jwt, source} = {...defaults, ...props}
return {host, jwt, source}
}
describe('fetchFolders', () => {
@ -256,38 +257,44 @@ describe('Upload data actions', () => {
type: 'video/mov'
}
}
let k5uploaderstub
beforeEach(() => {
moxios.install()
k5uploaderstub = sinon.stub(K5Uploader.prototype, 'loadUiConf').callsFake(() => 'mock')
})
afterEach(() => {
moxios.uninstall()
k5uploaderstub.restore()
})
it('uploads directly to notorious/kaltura', () => {
const baseState = setupState()
const store = spiedStore(baseState)
// I really just wanted to stub saveMediaRecording and assert that it's called,
// but sinon can't stub functions from es6 modules.
// The next best thing is to check that the K5Uploader is exercised
moxios.stubRequest('/api/v1/services/kaltura_session?include_upload_config=1', {
status: 200,
response: {
ks: 'averylongstring',
subp_id: '0',
partner_id: '9',
uid: '1234_567',
serverTime: 1234,
kaltura_setting: {
uploadUrl: 'url.url.url',
entryUrl: 'url.url.url',
uiconfUrl: 'url.url.url',
partnerData: 'data from our partners'
moxios.stubRequest(
'http://host:port/api/v1/services/kaltura_session?include_upload_config=1',
{
status: 200,
response: {
ks: 'averylongstring',
subp_id: '0',
partner_id: '9',
uid: '1234_567',
serverTime: 1234,
kaltura_setting: {
uploadUrl: 'url.url.url',
entryUrl: 'url.url.url',
uiconfUrl: 'url.url.url',
partnerData: 'data from our partners'
}
}
}
})
const k5uploaderstub = sinon.stub(K5Uploader.prototype, 'loadUiConf').callsFake(() => 'mock')
)
const baseState = setupState()
const store = spiedStore(baseState)
return store.dispatch(actions.uploadToMediaFolder('media', fakeFileMetaData)).then(() => {
sinon.assert.called(k5uploaderstub)
})
@ -544,7 +551,7 @@ describe('Upload data actions', () => {
const R = global.Response
beforeEach(() => {
if (typeof Response !== 'function') {
global.Response = function(body, status) {
global.Response = function (body, status) {
this.status = status
this.json = () => {
return Promise.resolve(JSON.parse(body))

View File

@ -18,7 +18,7 @@
import assert from 'assert'
import sinon from 'sinon'
import RceApiSource from '../../../src/sidebar/sources/api'
import RceApiSource, {headerFor, originFromHost} from '../../../src/sidebar/sources/api'
import fetchMock from 'fetch-mock'
import * as fileUrl from '../../../src/common/fileUrl'
@ -639,4 +639,40 @@ describe('sources/api', () => {
})
})
})
describe('headerFor', () => {
it('returns an authorization header', () => {
assert.deepStrictEqual(headerFor('the_jwt'), {
Authorization: 'Bearer the_jwt'
})
})
})
describe('originFromHost', () => {
// this logic was factored out from baseUri, so the logic is tested
// there too.
it('uses the incoming http(s) protocol if present', () => {
assert.strictEqual(originFromHost('http://host:port'), 'http://host:port', 'echoes http')
assert.strictEqual(originFromHost('https://host:port'), 'https://host:port', 'echoes https')
assert.strictEqual(originFromHost('host:port', {}), '//host:port', 'no protocol')
})
it('uses the windowOverride protocol if present', () => {
const win = {
location: {
protocol: 'https:'
}
}
assert.strictEqual(
originFromHost('http://host:port', win),
'http://host:port',
'use provided protocol'
)
assert.strictEqual(
originFromHost('host:port', win),
'https://host:port',
'use window protocol'
)
})
})
})

View File

@ -32,6 +32,7 @@ import {
MediaCaptureStrings,
SelectStrings
} from '../../helpers/UploadMediaTranslations'
import {getRCSAuthenticationHeaders, getRCSOriginFromHost} from '@instructure/canvas-rce'
import {Billboard} from '@instructure/ui-billboard'
import {Button} from '@instructure/ui-buttons'
@ -129,7 +130,11 @@ export default class MediaAttempt extends React.Component {
/>
</div>
) : (
<MediaPlayer tracks={mediaTracks} sources={mediaObject.mediaSources} />
<MediaPlayer
tracks={mediaTracks}
sources={mediaObject.mediaSources}
captionPosition="bottom"
/>
)}
</Flex.Item>
<Flex.Item overflowY="visible" margin="medium 0">
@ -171,8 +176,12 @@ export default class MediaAttempt extends React.Component {
<UploadMedia
onUploadComplete={this.onComplete}
onDismiss={this.onDismiss}
contextId={this.props.assignment.env.courseId}
contextType="course"
rcsConfig={{
contextId: this.props.assignment.env.courseId,
contextType: 'course',
origin: getRCSOriginFromHost(ENV.RICH_CONTENT_APP_HOST),
headers: getRCSAuthenticationHeaders(ENV.JWT)
}}
open={this.state.mediaModalOpen}
tabs={{embed: false, record: true, upload: true}}
uploadMediaTranslations={{UploadMediaStrings, MediaCaptureStrings, SelectStrings}}

248
yarn.lock
View File

@ -1456,10 +1456,10 @@
"@babel/helper-module-imports" "^7.8.3"
babel-plugin-macros "^2.8.0"
"@instructure/console@^7.4.4", "@instructure/console@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/console/-/console-7.5.0.tgz#af7b155a93fc1b1eb8a86332e313c81809542a15"
integrity sha512-S+Wu6C4EZQcOLdd83If9RkYNr8M2KZaQliQCJ2HaggsgjywxHmqRKLW0rwzQMr7i20+ocn9/HNUo84lMYyDycg==
"@instructure/console@^7.4.4", "@instructure/console@^7.5.0", "@instructure/console@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/console/-/console-7.6.0.tgz#fa4be013ace2c9f1402918debe799ebdc50715b4"
integrity sha512-SMQsaFzbFMBdAD0ZWt9BYG75AbF1Nw1KY6tWw0uQ/H+hvKTfl2Nq4g9Y7vnam5qIAwIA5RzbiFcJjYs5TyvjVQ==
dependencies:
"@babel/helper-annotate-as-pure" "^7.8.3"
"@babel/helper-module-imports" "^7.8.3"
@ -1479,10 +1479,10 @@
dependencies:
"@babel/runtime" "^7"
"@instructure/debounce@^7.4.4", "@instructure/debounce@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/debounce/-/debounce-7.5.0.tgz#811a1ea0c3ce8478f9a13e8ce816c15b1f68babd"
integrity sha512-4MJQBYsrWJSUqDzqtpMhQjuxlVY8dZ79quVLUOlJvnOtw6oPtLFdFw36/+nni5YQP6kXMpjcwO/m6QVhqG0F7A==
"@instructure/debounce@^7.4.4", "@instructure/debounce@^7.5.0", "@instructure/debounce@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/debounce/-/debounce-7.6.0.tgz#67e2e1331735cbbbf8f3d991cbdb61ae11128323"
integrity sha512-IWGtjI5laU5ffBc50CRoube4t5Hi4Az31TgV4dZY6d7ctPRUugbRmu0iz5y9OP54MCrXDoZ8M3Xr0qdsPuF10g==
dependencies:
"@babel/runtime" "^7.9.2"
@ -1648,18 +1648,18 @@
resolved "https://registry.yarnpkg.com/@instructure/redux-service-middleware/-/redux-service-middleware-1.0.0.tgz#16e18ad18d28a24e8f6f8e5eae66c0e638dd75d5"
integrity sha512-Bb/GzXhgWXnCI5UPjEwsVHHsTKIq1PwU8GXVH9dqkkp+0vzQzpFLsVUy2AUasxdMIHphe1bKq/+aR34/x7/PEw==
"@instructure/ui-a11y-content@7", "@instructure/ui-a11y-content@^7", "@instructure/ui-a11y-content@^7.3.0", "@instructure/ui-a11y-content@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-a11y-content/-/ui-a11y-content-7.5.0.tgz#50402fbbf257b7f7704d880ea59ea64ad2efff4e"
integrity sha512-rvAi720HezEE57rnw1IjRKnMDBJw00TiV3yCoaauXQ+qDDckzcnXQn/FsG6CetXOO+BLvFq03delPC3wp/8z2A==
"@instructure/ui-a11y-content@7", "@instructure/ui-a11y-content@^7", "@instructure/ui-a11y-content@^7.3.0", "@instructure/ui-a11y-content@^7.5.0", "@instructure/ui-a11y-content@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-a11y-content/-/ui-a11y-content-7.6.0.tgz#0a1658e5423db77b858ebf57d045fa671f999298"
integrity sha512-mATYmvi/TpcqoP49DekidW+2mGoFjQe9Rh2iUklguSxzfrchTTd3UFPM4xZ0n3SlXyojyASpgmeZel8ajODUaw==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-themeable" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/uid" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-themeable" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
"@instructure/uid" "^7.6.0"
keycode "^2"
prop-types "^15"
@ -2143,10 +2143,10 @@
prop-types "^15"
react-codemirror2 "^7.1.0"
"@instructure/ui-color-utils@7", "@instructure/ui-color-utils@^7", "@instructure/ui-color-utils@^7.3.0", "@instructure/ui-color-utils@^7.4.4", "@instructure/ui-color-utils@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-color-utils/-/ui-color-utils-7.5.0.tgz#d90f4b90e6d458cfe608cb9c65c75cbb322ded6a"
integrity sha512-Vq9DybIyEj3qJAEolnmpx3hhFsvJMQiYiMYvB63h359wpRIZflRNiXjMtHXzzUaAyhb/14HYI6t3X1NYTv+2KA==
"@instructure/ui-color-utils@7", "@instructure/ui-color-utils@^7", "@instructure/ui-color-utils@^7.3.0", "@instructure/ui-color-utils@^7.4.4", "@instructure/ui-color-utils@^7.5.0", "@instructure/ui-color-utils@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-color-utils/-/ui-color-utils-7.6.0.tgz#45238c2189fcb4cbaf747feee99a3d2e9bdc2b0e"
integrity sha512-ltywWefY9azn2W7Uc5+AhVL6au4jF82qCfL+jEmHIwC/LTbUMsvoKJ2r+6pFFgGTzy1fAcxniskpV7UP9HYt1g==
dependencies:
"@babel/runtime" "^7.9.2"
tinycolor2 "^1.4.1"
@ -2237,10 +2237,10 @@
dependencies:
"@babel/runtime" "^7"
"@instructure/ui-decorator@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-decorator/-/ui-decorator-7.5.0.tgz#a7863aebe94863beed91c52a84194fac4384b98e"
integrity sha512-sDrwAYytnwXB+94ekj7COHQzWc65+9FKx/w8j9Cb9mpnGOsZsZK47rXCwHrqAdYc4zVeJmrWFx/YMBYvi+HYvg==
"@instructure/ui-decorator@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-decorator/-/ui-decorator-7.6.0.tgz#16862bf4b3b99d3e3cc4714dd61ec2abe5559efe"
integrity sha512-5GO547SaUUu1n5SuICRDLfjQV7bnvko3kqZMacqVAn1MaqePI6ivd8d3ZVZaRyB72BJZqsghfjVEBYsedw7KCA==
dependencies:
"@babel/runtime" "^7.9.2"
@ -2278,13 +2278,13 @@
"@babel/runtime" "^7.9.2"
"@instructure/console" "^6.27.0"
"@instructure/ui-dom-utils@^7.3.0", "@instructure/ui-dom-utils@^7.4.4", "@instructure/ui-dom-utils@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-dom-utils/-/ui-dom-utils-7.5.0.tgz#172f5832ad1f3831af9495d72c73a45d120c9dd0"
integrity sha512-tVeoYGN5MoxVhx6U2NJVlxsYbGDdg9wNFKupBB3QHK0L5YaC9AeHzHvhb8CSagUHuhPk7fKEilcR2uspK9z3Yw==
"@instructure/ui-dom-utils@^7.3.0", "@instructure/ui-dom-utils@^7.4.4", "@instructure/ui-dom-utils@^7.5.0", "@instructure/ui-dom-utils@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-dom-utils/-/ui-dom-utils-7.6.0.tgz#4833ff140205ca863de3ac5088fcdd91798a101d"
integrity sha512-oyFj2QcXU6Xyez+aVcq4mnwNb+vIWvvNlkoxCMl4MrJo7le/CylHsefSnQR5ZNolPnO4/OE0fsIXAmfarzsS2A==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-editable@6":
version "6.27.0"
@ -2697,17 +2697,17 @@
moment-timezone "^0.5"
prop-types "^15"
"@instructure/ui-i18n@^7.4.4", "@instructure/ui-i18n@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-i18n/-/ui-i18n-7.5.0.tgz#620d3cbe53825926e5801a91b7034430ea3a5000"
integrity sha512-JKyoCjp/9bC1ICaqkfmyn5EsBypXl/TXo51wy0FJJ9QtfRUbgj/wt86Q4iIRltABP3yEwHVcV0fny++uA8J9PA==
"@instructure/ui-i18n@^7.4.4", "@instructure/ui-i18n@^7.5.0", "@instructure/ui-i18n@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-i18n/-/ui-i18n-7.6.0.tgz#e1771af88dccc201b424516698719a2598ebec07"
integrity sha512-pQ8+oUGU5IkKKEhwyIMoYhm7oRV5zonBTU0gLqxPvx/l+cekezxbAq4iVwZzsgNd9a7meV7Vc1q0aKANv4Zjvg==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/ui-decorator" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-prop-types" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/ui-decorator" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-prop-types" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
decimal.js "^10"
moment-timezone "^0.5"
prop-types "^15"
@ -3373,16 +3373,16 @@
"@instructure/ui-utils" "^6.27.0"
prop-types "^15"
"@instructure/ui-portal@7", "@instructure/ui-portal@^7.4.4", "@instructure/ui-portal@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-portal/-/ui-portal-7.5.0.tgz#b354a631a056b85980762c6b451058e7ef8b2aa4"
integrity sha512-ZiRz8mSy7R1/POuu1XocHQXp0yah3vfl7nMdsP6/UHiwAIiXcKhWQ8PUANgkMZNOzMTsHgkyVNDxoikGV9ZfVA==
"@instructure/ui-portal@7", "@instructure/ui-portal@^7.4.4", "@instructure/ui-portal@^7.5.0", "@instructure/ui-portal@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-portal/-/ui-portal-7.6.0.tgz#b45f58573b0cdc525cb89dec41ac747f0568b292"
integrity sha512-NFgn9q0DOnq6umOsakFis5/m8FsQXKdA89IDeG5PZ4Ha1BTQ7AZVZQa5MyhnwAHpMvo6RaB9P/NRRxgLCbP7Ww==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/ui-i18n" "^7.5.0"
"@instructure/ui-prop-types" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/ui-i18n" "^7.6.0"
"@instructure/ui-prop-types" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
prop-types "^15"
"@instructure/ui-portal@^5.52.3":
@ -3415,21 +3415,21 @@
classnames "^2"
prop-types "^15"
"@instructure/ui-position@^7.4.4", "@instructure/ui-position@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-position/-/ui-position-7.5.0.tgz#2913eb32bee58bbc727424477433d7b33ce58061"
integrity sha512-2hPin+XBktVTH7sqExatdTjD87nlJRnTqr0jqGWZD0/gN9uZsQj9nAQEVLA3BRkBBNay0R6p3OOQ4uObMi+jEw==
"@instructure/ui-position@^7.4.4", "@instructure/ui-position@^7.5.0", "@instructure/ui-position@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-position/-/ui-position-7.6.0.tgz#4b96d97b16c7319733ed0159a7876fa940813c8c"
integrity sha512-7mmuIoMo2Pw4Ar9CaTmCOyE0maYrg6Sf+zLaPMEeRN+p1ueQfRrBnpKi9r9w6PdZdCWPq/BTOKD8p2i2NFkTMQ==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/debounce" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-portal" "^7.5.0"
"@instructure/ui-prop-types" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-testable" "^7.5.0"
"@instructure/ui-themeable" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/uid" "^7.5.0"
"@instructure/debounce" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-portal" "^7.6.0"
"@instructure/ui-prop-types" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-testable" "^7.6.0"
"@instructure/ui-themeable" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
"@instructure/uid" "^7.6.0"
classnames "^2"
prop-types "^15"
@ -3491,10 +3491,26 @@
classnames "^2"
prop-types "^15"
"@instructure/ui-prop-types@7", "@instructure/ui-prop-types@^7.4.4", "@instructure/ui-prop-types@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-prop-types/-/ui-prop-types-7.5.0.tgz#a014873e2a74966b98be28680b7eb7b88f74514b"
integrity sha512-VoBtRgYx2pme4BAD6/ZZjNYrhfu3o8Xn9mGCTCSpRBbyh82GbbyiqgS+5rtTfwLRxoSYhkXDcm9kTFRAGaM3IQ==
"@instructure/ui-progress@^7":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-progress/-/ui-progress-7.6.0.tgz#ce40b451a978c17424079e0191a8aa8967d1ba40"
integrity sha512-9glVNHk642lGPzuvseTYHe6GPw8SqX9P51B2WIA2MITzIlX2pGkxospmMCRfv4n0DmrndfXKbKcNplfAp2AyLg==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.6.0"
"@instructure/ui-a11y-content" "^7.6.0"
"@instructure/ui-color-utils" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-testable" "^7.6.0"
"@instructure/ui-themeable" "^7.6.0"
"@instructure/ui-view" "^7.6.0"
classnames "^2"
prop-types "^15"
"@instructure/ui-prop-types@7", "@instructure/ui-prop-types@^7.4.4", "@instructure/ui-prop-types@^7.5.0", "@instructure/ui-prop-types@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-prop-types/-/ui-prop-types-7.6.0.tgz#d39b373527805c0d57cde7ab609c04618de5c575"
integrity sha512-TDs/rvnSZG5JbmxloTncVv0Mqqa2X/OkEbwurRC67B57BsPCixPkkZS8LU3cCChe8xOi3A3xQso9amcmvqLL0w==
dependencies:
"@babel/runtime" "^7.9.2"
prop-types ">= 15.7.0"
@ -3599,17 +3615,17 @@
prop-types "^15"
react-lifecycles-compat "^3.0.4"
"@instructure/ui-react-utils@7", "@instructure/ui-react-utils@^7", "@instructure/ui-react-utils@^7.3.0", "@instructure/ui-react-utils@^7.4.4", "@instructure/ui-react-utils@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-react-utils/-/ui-react-utils-7.5.0.tgz#505c503c1a339b9f67a1ff8b247bb253469e8489"
integrity sha512-J4WQLNi/3G3O447V0jNl7ioIN9xwI9poxqgEhjDr7DzdnGp2YNZL4jjVD0zJfKhtBKowwbHYOgqiiSz1RiN++Q==
"@instructure/ui-react-utils@7", "@instructure/ui-react-utils@^7", "@instructure/ui-react-utils@^7.3.0", "@instructure/ui-react-utils@^7.4.4", "@instructure/ui-react-utils@^7.5.0", "@instructure/ui-react-utils@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-react-utils/-/ui-react-utils-7.6.0.tgz#0205b1d89ef165482bc1ea5707ecbc7975ce3252"
integrity sha512-kURXO0ivkMiIrhz2VDf3GhZ1gu2OE9lr4FGkMulpYy2IbAFBryL2YYCcE2QZ/cce7K56yRdEl/GEaTIwXcRFEA==
dependencies:
"@babel/runtime" "^7.9.2"
"@emotion/is-prop-valid" "^0.8.3"
"@instructure/console" "^7.5.0"
"@instructure/ui-decorator" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-decorator" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
prop-types "^15"
"@instructure/ui-responsive@6":
@ -3789,10 +3805,10 @@
"@babel/runtime" "^7.9.2"
glamor "^2.20.40"
"@instructure/ui-stylesheet@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-stylesheet/-/ui-stylesheet-7.5.0.tgz#a1b8d8333b669061abca0fc33a13b2fcf5db9512"
integrity sha512-ReBbFwLomCFPpR87KjD2d3U42FOEASu9Ukkv3crEEVkxeQnp6Cep92EalzVqs9qkVBb8l3E/MfC13sy0K0fCLQ==
"@instructure/ui-stylesheet@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-stylesheet/-/ui-stylesheet-7.6.0.tgz#21ead006eaaf340155b1625bf03e03119edff3b3"
integrity sha512-cW7qWER6Y3Am6lttLOa0TW2Jn2HoJ/0ZumoJ54CtS8r+Mj5EPWd7039UuJkxm9i1nJljyYumdtYhWkokNxuc2Q==
dependencies:
"@babel/runtime" "^7.9.2"
glamor "^2.20.40"
@ -4026,13 +4042,13 @@
"@babel/runtime" "^7.9.2"
"@instructure/ui-decorator" "^6.27.0"
"@instructure/ui-testable@^7.3.0", "@instructure/ui-testable@^7.4.4", "@instructure/ui-testable@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-testable/-/ui-testable-7.5.0.tgz#45b6d9a7db3f25fdf837cc43d0163071d0ea5787"
integrity sha512-794VksT9+/dHKgbIri+BMx5/8bmYGqHHYainwJmHRDH7/CjdfmzfzW01G6gBypFU1SywkYZNOQuYgOLhz5PVqQ==
"@instructure/ui-testable@^7.3.0", "@instructure/ui-testable@^7.4.4", "@instructure/ui-testable@^7.5.0", "@instructure/ui-testable@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-testable/-/ui-testable-7.6.0.tgz#deaa3421e7b0d901707bc8c6085a01a198e2dfa8"
integrity sha512-fN2bK7K60OA4oAopajU6YqXo6RGAHIZjcoTAKhOJt1ThPLaYHkgvByPx7miV6/zYUfHO2mYnVx4VTPix/Z94xA==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/ui-decorator" "^7.5.0"
"@instructure/ui-decorator" "^7.6.0"
"@instructure/ui-text-area@6", "@instructure/ui-text-area@^6.27.0":
version "6.27.0"
@ -4158,20 +4174,20 @@
"@instructure/uid" "^6.27.0"
prop-types "^15"
"@instructure/ui-themeable@7", "@instructure/ui-themeable@^7", "@instructure/ui-themeable@^7.3.0", "@instructure/ui-themeable@^7.4.4", "@instructure/ui-themeable@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-themeable/-/ui-themeable-7.5.0.tgz#7afaab26b7870adc3ce38f73e8616873089a43e2"
integrity sha512-0yXjDcavzanCIoMUC88zZv3vE/pCXzxIKGUn1I1wTHcHq3B+SXsc4Eu8kr7DwwMZToOStXJo0mFpY+/MYZGbrw==
"@instructure/ui-themeable@7", "@instructure/ui-themeable@^7", "@instructure/ui-themeable@^7.3.0", "@instructure/ui-themeable@^7.4.4", "@instructure/ui-themeable@^7.5.0", "@instructure/ui-themeable@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-themeable/-/ui-themeable-7.6.0.tgz#a72e73eb258fa5c452e7afa763c1803bef65c71e"
integrity sha512-hmHC/C04iw9dAtm+URlceJ9ff5SwpblxMB3Cp+P084kQ+M34TpWoAkWzIx7UEZR41dWMQ+jRhNzDm9ecQTkz6Q==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.5.0"
"@instructure/ui-color-utils" "^7.5.0"
"@instructure/ui-decorator" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-stylesheet" "^7.5.0"
"@instructure/ui-utils" "^7.5.0"
"@instructure/uid" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-color-utils" "^7.6.0"
"@instructure/ui-decorator" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-stylesheet" "^7.6.0"
"@instructure/ui-utils" "^7.6.0"
"@instructure/uid" "^7.6.0"
prop-types "^15"
"@instructure/ui-themeable@^5", "@instructure/ui-themeable@^5.52.3":
@ -4434,14 +4450,14 @@
escape-html "^1"
prop-types "^15"
"@instructure/ui-utils@7", "@instructure/ui-utils@^7", "@instructure/ui-utils@^7.4.4", "@instructure/ui-utils@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-utils/-/ui-utils-7.5.0.tgz#55cd2315651c2f9415b831c712b712f4166cdc7c"
integrity sha512-pl+sREpuN6P37Gv6M4igbZ1kQVwaCmBZKCBkl5my8Pvkp0st0gyyMGFXjaBJpBj2lOyBdYYbmbBWSYGspjOpRw==
"@instructure/ui-utils@7", "@instructure/ui-utils@^7", "@instructure/ui-utils@^7.4.4", "@instructure/ui-utils@^7.5.0", "@instructure/ui-utils@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-utils/-/ui-utils-7.6.0.tgz#d745df92d76f8e2a25a5d11016715809d47c903a"
integrity sha512-XLpmTwCXdmR2qur5nq+pj7A7mU62Aqm+KBsstQ3RPt/oHoiNayjjY+dbXKtcH3c47gpCof9uX2xlsfnkOzaVrg==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
bowser "^1.9.4"
fast-deep-equal "^2"
json-stable-stringify "^1.0.1"
@ -4496,20 +4512,20 @@
classnames "^2"
prop-types "^15"
"@instructure/ui-view@7", "@instructure/ui-view@^7", "@instructure/ui-view@^7.4.4", "@instructure/ui-view@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-view/-/ui-view-7.5.0.tgz#a6cac70fea148023ff41283e3fc980c26ed3886a"
integrity sha512-P1DyzSdMIU9bi8gj4sUqk+KTrCdhigu5ZalbAnzZImVNzeWJ0GW+1scDFxCYhWMzNd9ptMCewQ0GSAEI440vOQ==
"@instructure/ui-view@7", "@instructure/ui-view@^7", "@instructure/ui-view@^7.4.4", "@instructure/ui-view@^7.5.0", "@instructure/ui-view@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/ui-view/-/ui-view-7.6.0.tgz#5b0de5e0dfea00bcfad973bce806d2441e3962d6"
integrity sha512-7fJtCYfB+CeRGidx/Fm48fOr1brVERSuG4EAAfplVA78Xc453Zn7SSiclF2xglE3OHCXSlIY62O074NXfvJfKw==
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/console" "^7.5.0"
"@instructure/ui-color-utils" "^7.5.0"
"@instructure/ui-dom-utils" "^7.5.0"
"@instructure/ui-i18n" "^7.5.0"
"@instructure/ui-position" "^7.5.0"
"@instructure/ui-prop-types" "^7.5.0"
"@instructure/ui-react-utils" "^7.5.0"
"@instructure/ui-themeable" "^7.5.0"
"@instructure/console" "^7.6.0"
"@instructure/ui-color-utils" "^7.6.0"
"@instructure/ui-dom-utils" "^7.6.0"
"@instructure/ui-i18n" "^7.6.0"
"@instructure/ui-position" "^7.6.0"
"@instructure/ui-prop-types" "^7.6.0"
"@instructure/ui-react-utils" "^7.6.0"
"@instructure/ui-themeable" "^7.6.0"
classnames "^2"
prop-types "^15"
@ -4520,10 +4536,10 @@
dependencies:
"@babel/runtime" "^7.9.2"
"@instructure/uid@7", "@instructure/uid@^7.3.0", "@instructure/uid@^7.4.4", "@instructure/uid@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@instructure/uid/-/uid-7.5.0.tgz#2cdffb4567d5c9ee263f1864cb5bff6f087f70ce"
integrity sha512-b7ynsDpmDAC3nvFTarf0N3LHe1IJMwQ8cJQBH0euKtwkfboOv617pAy+npZRuTNj+XZwpFGTKltujQh91RUQcQ==
"@instructure/uid@7", "@instructure/uid@^7.3.0", "@instructure/uid@^7.4.4", "@instructure/uid@^7.5.0", "@instructure/uid@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@instructure/uid/-/uid-7.6.0.tgz#45e316b01ef3bd692fed5032d1ac524a587cd892"
integrity sha512-vEOWlKqLo+AXVfVLbETeldCW5moxXicIXDs1jSxcEBaIP/7DnDwQe3uZJjyDAkNMcKz3lnpYOawS7eu8nw1UcQ==
dependencies:
"@babel/runtime" "^7.9.2"