IM: write file as icon when viewing from All Files

Selecting an icon maker file from the side tray when viewing
"All Files", its inserted into the RCE as an icon maker icon

fixes MAT-987
flag=buttons_and_icons_root_account
flag=buttons_and_icons_cropper

* Will also need the following canvas-rce-api commit running locally:
https://gerrit.instructure.com/c/canvas-rce-api/+/302455
** Check that commit first on how to test the RCS for backwards
compatibility. This backwards compatibility test should be done before
getting these changes into your local dev environment.

Test Plan
- Go to the RCE and select "Saved Icon Maker Icons" from the IM button
menu
- Select the "Icon Maker Icons" dropdown in the side tray and change it
to "All"
- Navigate to Course Files > Icon Maker Icons
- Select an icon from the list to insert it into the RCE
- Select the newly added icon in the RCE
* VERIFY:
1. The newly added icon has IM-only context menus: "Edit Icon" and
"Icon Options"
2. Edit icon and saving changes works as expected
3. "Icon Options" allows for changing alt text settings
4. Also verify that adding non-Icon-Maker-icons works as before using
All Files. E.g. add an image, select the image in the RCE and you
should get the "Image Options" context menu item.

Change-Id: I862ce51021955624b354a3dfe7f24db8251209b2
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/302435
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jake Oeding <jake.oeding@instructure.com>
QA-Review: Jake Oeding <jake.oeding@instructure.com>
Product-Review: David Lyons <lyons@instructure.com>
This commit is contained in:
Joe Hernandez 2022-09-27 16:16:28 -05:00
parent 8bd46cb657
commit f886aadd76
11 changed files with 212 additions and 95 deletions

View File

@ -30,7 +30,7 @@ module Api::V1::Folders
def folder_json(folder, user, session, opts = {})
can_view_hidden_files = opts.key?(:can_view_hidden_files) ? opts[:can_view_hidden_files] : folder.grants_right?(user, :update)
json = api_json(folder, user, session,
only: %w[id name full_name position parent_folder_id context_type context_id unlock_at lock_at created_at updated_at])
only: %w[id name full_name position parent_folder_id context_type context_id unlock_at lock_at created_at updated_at category])
if folder
if opts[:master_course_restricted_folder_ids]&.include?(folder.id)
json["is_master_course_child_content"] = true

View File

@ -30,11 +30,10 @@ import {Footer} from './CreateIconMakerForm/Footer'
import {buildStylesheet, buildSvg} from '../svg'
import {statuses, useSvgSettings} from '../svg/settings'
import {defaultState, actions} from '../reducers/svgSettings'
import {ICON_MAKER_ATTRIBUTE, ICON_MAKER_DOWNLOAD_URL_ATTR} from '../svg/constants'
import {FixedContentTray} from '../../shared/FixedContentTray'
import {useStoreProps} from '../../shared/StoreContext'
import formatMessage from '../../../../format-message'
import buildDownloadUrl from '../../shared/buildDownloadUrl'
import addIconMakerAttributes from '../utils/addIconMakerAttributes'
import {validIcon} from '../utils/iconValidation'
import {IconMakerFormHasChanges} from '../utils/IconMakerFormHasChanges'
import bridge from '../../../../bridge'
@ -240,7 +239,6 @@ export function IconMakerTray({editor, onUnmount, editing, rcsConfig}) {
const writeIconToRCE = ({url, display_name}) => {
const {alt, isDecorative, externalStyle, externalWidth, externalHeight} = settings
const imageAttributes = {
alt_text: alt,
display_name,
@ -255,12 +253,7 @@ export function IconMakerTray({editor, onUnmount, editing, rcsConfig}) {
}
// Mark the image as an icon maker icon.
imageAttributes[ICON_MAKER_ATTRIBUTE] = true
// URL to fetch the SVG from when loading the Edit tray.
// We can't use the 'src' because Canvas will re-write the
// source attribute to a URL that is not cross-origin friendly.
imageAttributes[ICON_MAKER_DOWNLOAD_URL_ATTR] = buildDownloadUrl(url)
addIconMakerAttributes(imageAttributes)
bridge.embedImage(imageAttributes)
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2022 - 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 addIconMakerAttributes from '../addIconMakerAttributes'
import {ICON_MAKER_ATTRIBUTE, ICON_MAKER_DOWNLOAD_URL_ATTR} from '../../svg/constants'
import buildDownloadUrl from '../../../shared/buildDownloadUrl'
describe('addIconMakerAttributes', () => {
const url = 'http://canvas.tests/files/1/download'
let imageAttributes
beforeEach(() => {
imageAttributes = {src: url}
})
it('adds the "data-inst-icon-maker-icon" attribute with a value of "true"', () => {
addIconMakerAttributes(imageAttributes)
expect(imageAttributes[ICON_MAKER_ATTRIBUTE]).toBe(true)
})
it('adds the "data-download-url" attribute with the correct url for IM icons', () => {
addIconMakerAttributes(imageAttributes)
const expectedDownloadUrl = buildDownloadUrl(imageAttributes.src)
expect(imageAttributes[ICON_MAKER_DOWNLOAD_URL_ATTR]).toBe(expectedDownloadUrl)
})
})

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2022 - 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 {ICON_MAKER_ATTRIBUTE, ICON_MAKER_DOWNLOAD_URL_ATTR} from '../svg/constants'
import buildDownloadUrl from '../../shared/buildDownloadUrl'
const addIconMakerAttributes = (imageAttributes: {src: string}) => {
imageAttributes[ICON_MAKER_ATTRIBUTE] = true
imageAttributes[ICON_MAKER_DOWNLOAD_URL_ATTR] = buildDownloadUrl(imageAttributes.src)
}
export default addIconMakerAttributes

View File

@ -23,6 +23,7 @@ import {View} from '@instructure/ui-view'
import {downloadToWrap} from '../../../common/fileUrl'
import {mediaPlayerURLFromFile} from './fileTypeUtils'
import RceApiSource from '../../../rcs/api'
import addIconMakerAttributes from '../instructure_icon_maker/utils/addIconMakerAttributes'
// TODO: should find a better way to share this code
import FileBrowser from '../../../canvasFileBrowser/FileBrowser'
@ -31,7 +32,7 @@ import {isPreviewable} from './Previewable'
RceFileBrowser.propTypes = {
onFileSelect: func.isRequired,
onAllFilesLoading: func.isRequired,
searchString: string.isRequired
searchString: string.isRequired,
}
export default function RceFileBrowser(props) {
@ -42,7 +43,7 @@ export default function RceFileBrowser(props) {
new RceApiSource({
jwt,
refreshToken,
host
host,
})
)
}, [host, jwt, refreshToken, source])
@ -51,24 +52,31 @@ export default function RceFileBrowser(props) {
const content_type = fileInfo.api.type
const canPreview = isPreviewable(content_type)
const clazz = classnames('instructure_file_link', {
instructure_scribd_file: canPreview,
inline_disabled: true
})
const url = downloadToWrap(fileInfo.src)
const embedded_iframe_url = mediaPlayerURLFromFile(fileInfo.api)
onFileSelect({
let onFileSelectParams = {
name: fileInfo.name,
title: fileInfo.name,
href: url,
embedded_iframe_url,
media_id: fileInfo.api.embed?.id,
target: '_blank',
class: clazz,
content_type
})
content_type,
}
if (fileInfo.api?.category === 'icon_maker_icons') {
onFileSelectParams.src = fileInfo.api.url
addIconMakerAttributes(onFileSelectParams)
} else {
// do not add this to icon maker icons
const clazz = classnames('instructure_file_link', {
instructure_scribd_file: canPreview,
inline_disabled: true,
})
onFileSelectParams = {...onFileSelectParams, class: clazz}
}
onFileSelect(onFileSelectParams)
}
return (

View File

@ -24,19 +24,22 @@ import FileBrowser from '../../../../canvasFileBrowser/FileBrowser'
jest.mock('../../../../canvasFileBrowser/FileBrowser', () => {
return jest.fn(() => 'Files Browser')
})
jest.mock('../../../../bridge')
describe('RceFileBrowser', () => {
const onAllFilesLoading = jest.fn()
const props = {searchString: '', onAllFilesLoading}
afterEach(() => FileBrowser.mockClear())
it('invokes onFileSelect callback with appropriate data when a file is selected', () => {
const onFileSelect = jest.fn()
render(<RceFileBrowser onFileSelect={onFileSelect} />)
render(<RceFileBrowser onFileSelect={onFileSelect} {...props} />)
// This is the selectFile prop passed to the Canvas FileBrowser that we mocked above
const selectFile = FileBrowser.mock.calls[0][0].selectFile
selectFile({
name: 'a file',
src: '/file/download',
api: {url: '/file/download?download_frd=1', type: 'application/pdf'}
api: {url: '/file/download?download_frd=1', type: 'application/pdf'},
})
expect(onFileSelect).toHaveBeenCalledWith({
name: 'a file',
@ -45,13 +48,13 @@ describe('RceFileBrowser', () => {
embedded_iframe_url: undefined,
content_type: 'application/pdf',
target: '_blank',
class: 'instructure_file_link instructure_scribd_file inline_disabled'
class: 'instructure_file_link instructure_scribd_file inline_disabled',
})
})
it('plumbs the media_id when a video file is selected', () => {
const onFileSelect = jest.fn()
render(<RceFileBrowser onFileSelect={onFileSelect} />)
render(<RceFileBrowser onFileSelect={onFileSelect} {...props} />)
// This is the selectFile prop passed to the Canvas FileBrowser that we mocked above
const selectFile = FileBrowser.mock.calls[0][0].selectFile
selectFile({
@ -60,8 +63,8 @@ describe('RceFileBrowser', () => {
api: {
url: '/file/download?download_frd=1',
type: 'video/mp4',
embed: { id: 'm-deadbeef' }
}
embed: {id: 'm-deadbeef'},
},
})
expect(onFileSelect).toHaveBeenCalledWith({
name: 'a video',
@ -71,7 +74,34 @@ describe('RceFileBrowser', () => {
media_id: 'm-deadbeef',
content_type: 'video/mp4',
target: '_blank',
class: 'instructure_file_link inline_disabled'
class: 'instructure_file_link inline_disabled',
})
})
describe('when the selected file has category=icon_maker_icon', () => {
it('adds icon maker icon attributes to onFileSelect object param', () => {
const onFileSelect = jest.fn()
render(<RceFileBrowser onFileSelect={onFileSelect} {...props} />)
// This is the selectFile prop passed to the Canvas FileBrowser that we mocked above
const selectFile = FileBrowser.mock.calls[0][0].selectFile
const fullUrl = 'http://dev.env/files/123/download'
selectFile({
name: 'a file',
src: '/files/123/download',
api: {
url: fullUrl,
name: 'awesome-icon',
category: 'icon_maker_icons',
type: 'image/jpg',
},
})
expect(onFileSelect).toHaveBeenCalledWith(
expect.objectContaining({
'data-download-url': `${fullUrl}?icon_maker_icon=1`,
'data-inst-icon-maker-icon': true,
src: fullUrl,
})
)
})
})
})

View File

@ -27,7 +27,7 @@ beforeEach(() => {
refreshToken: callback => {
callback('freshJWT')
},
alertFunc: jest.fn()
alertFunc: jest.fn(),
})
apiSource.fetchPage = jest.fn()
@ -44,9 +44,9 @@ describe('fetchImages()', () => {
const standardProps = {
contextType: 'course',
images: {
course: {}
course: {},
},
sortBy: 'date'
sortBy: 'date',
}
const subject = () => apiSource.fetchImages(props)
@ -59,7 +59,7 @@ describe('fetchImages()', () => {
describe('with "category" set', () => {
props = {
category: 'uncategorized',
...standardProps
...standardProps,
}
})
@ -83,9 +83,26 @@ describe('fetchFilesForFolder()', () => {
fetchMock.mock('/api/files', '{"files": []}')
})
it('includes the "uncategorized" category in the request', async () => {
it('fetches folder files without query params if none supplied in props', async () => {
apiProps = {...apiProps}
await subject()
expect(apiSource.fetchPage).toHaveBeenCalledWith('/api/files?&category=uncategorized', 'theJWT')
expect(apiSource.fetchPage).toHaveBeenCalledWith('/api/files', 'theJWT')
})
it('fetches folder files using the per_page query param', async () => {
apiProps = {...apiProps, perPage: 5}
await subject()
expect(apiSource.fetchPage).toHaveBeenCalledWith('/api/files?per_page=5', 'theJWT')
})
it('fetches folder files using the encoded searchString query param', async () => {
apiProps = {...apiProps, perPage: 5, searchString: 'an awesome file'}
const encodedSearchString = encodeURIComponent(apiProps.searchString)
await subject()
expect(apiSource.fetchPage).toHaveBeenCalledWith(
`/api/files?per_page=5&search_term=${encodedSearchString}`,
'theJWT'
)
})
})
@ -102,9 +119,9 @@ describe('fetchMedia', () => {
media: {course: {}},
sortBy: {
sort: 'name',
dir: 'asc'
dir: 'asc',
},
contextId: 1
contextId: 1,
}
apiSource.apiFetch = jest.fn()
@ -133,8 +150,8 @@ describe('saveClosedCaptions()', () => {
{
language: {selectedOptionId: 'en'},
file: new Blob(['file contents'], {type: 'text/plain'}),
isNew: true
}
isNew: true,
},
]
maxBytes = undefined
})
@ -148,7 +165,7 @@ describe('saveClosedCaptions()', () => {
await subject()
expect(apiSource.alertFunc).toHaveBeenCalledWith({
text: 'Closed caption file must be less than 0.005 kb',
variant: 'error'
variant: 'error',
})
})
})

View File

@ -20,9 +20,7 @@ import 'isomorphic-fetch'
import {parse} from 'url'
import {saveClosedCaptions, CONSTANTS} from '@instructure/canvas-media'
import {downloadToWrap, fixupFileUrl} from '../common/fileUrl'
import formatMessage from '../format-message'
import alertHandler from '../rce/alertHandler'
import {DEFAULT_FILE_CATEGORY} from '../sidebar/containers/sidebarHandlers'
import buildError from './buildError'
export function headerFor(jwt) {
@ -65,7 +63,7 @@ function normalizeFileData(file) {
display_name: file.name,
...file,
// wrap the url
href: downloadToWrap(file.href || file.url)
href: downloadToWrap(file.href || file.url),
}
}
@ -108,7 +106,7 @@ class RceApiSource {
bookmark: this.uriFor(endpoint, props),
isLoading: false,
hasMore: true,
searchString: props.searchString
searchString: props.searchString,
}
}
@ -116,7 +114,7 @@ class RceApiSource {
return {
uploading: false,
folders: {},
formExpanded: false
formExpanded: false,
}
}
@ -130,9 +128,9 @@ class RceApiSource {
files: [],
bookmark: null,
isLoading: false,
hasMore: true
hasMore: true,
},
searchString: ''
searchString: '',
}
}
@ -144,7 +142,7 @@ class RceApiSource {
return {
searchResults: [],
searching: false,
formExpanded: false
formExpanded: false,
}
}
@ -172,7 +170,7 @@ class RceApiSource {
return this.apiFetch(uri, headerFor(this.jwt)).then(({bookmark, files}) => {
return {
bookmark,
files: files.map(f => fixupFileUrl(props.contextType, props.contextId, f))
files: files.map(f => fixupFileUrl(props.contextType, props.contextId, f)),
}
})
}
@ -187,7 +185,7 @@ class RceApiSource {
return this.fetchPage(uri).then(({bookmark, files}) => {
return {
bookmark,
files: files.map(normalizeFileData)
files: files.map(normalizeFileData),
}
})
}
@ -215,7 +213,7 @@ class RceApiSource {
: 'video',
context_code: mediaObject.contextCode,
title: mediaObject.title,
user_entered_title: mediaObject.userTitle
user_entered_title: mediaObject.userTitle,
}
return this.apiPost(this.baseUri('media_objects'), headerFor(this.jwt), body)
@ -237,7 +235,7 @@ class RceApiSource {
subtitles,
{
origin: originFromHost(apiProps.host),
headers: headerFor(apiProps.jwt)
headers: headerFor(apiProps.jwt),
},
maxBytes || CONSTANTS.CC_FILE_MAX_BYTES
).catch(e => {
@ -251,7 +249,7 @@ class RceApiSource {
fetchClosedCaptions(_mediaId) {
return Promise.resolve([
{locale: 'af', content: '1\r\n00:00:00,000 --> 00:00:01,251\r\nThis is the content\r\n'},
{locale: 'es', content: '1\r\n00:00:00,000 --> 00:00:01,251\r\nThis is the content\r\n'}
{locale: 'es', content: '1\r\n00:00:00,000 --> 00:00:01,251\r\nThis is the content\r\n'},
])
}
@ -268,10 +266,13 @@ class RceApiSource {
if (!bookmark) {
const perPageQuery = props.perPage ? `per_page=${props.perPage}` : ''
const categoryQuery = `category=${DEFAULT_FILE_CATEGORY}`
uri = `${props.filesUrl}?${perPageQuery}&${categoryQuery}${getSearchParam(
props.searchString
)}`
const searchParam = getSearchParam(props.searchString)
uri = `${props.filesUrl}`
uri += perPageQuery ? `?${perPageQuery}` : ''
if (searchParam) {
uri += perPageQuery ? `${searchParam}` : `?${searchParam}`
}
if (props.sortBy) {
uri += `${getSortParams(props.sortBy.sort, props.sortBy.order)}`
@ -291,7 +292,7 @@ class RceApiSource {
contextId,
contextType,
host: this.host,
jwt: this.jwt
jwt: this.jwt,
})
return this.fetchPage(uri)
}
@ -319,7 +320,7 @@ class RceApiSource {
return {
bookmark,
files: files.map(f => fixupFileUrl(props.contextType, props.contextId, f)),
searchString: props.searchString
searchString: props.searchString,
}
})
}
@ -333,7 +334,7 @@ class RceApiSource {
file: fileProps,
no_redirect: true,
onDuplicate: apiProps.onDuplicate,
category: apiProps.category
category: apiProps.category,
}
return this.apiPost(uri, headers, body)
@ -433,7 +434,7 @@ class RceApiSource {
const uri = this.addParamsIfPresent(`${base}/${id}`, {
replacement_chain_context_type,
replacement_chain_context_id
replacement_chain_context_id,
})
return this.apiFetch(uri, headers).then(normalizeFileData)
@ -497,7 +498,7 @@ class RceApiSource {
headers = {...headers, 'Content-Type': 'application/json'}
const fetchOptions = {
method,
headers
headers,
}
if (body) {
fetchOptions.body = JSON.stringify(body)

View File

@ -38,7 +38,7 @@ import {changeContext, changeSearchString, changeSortBy} from '../actions/filter
import {allFilesLoading} from '../actions/all_files'
import {get as getSession} from '../actions/session'
export const DEFAULT_FILE_CATEGORY = 'uncategorized'
const DEFAULT_FILE_CATEGORY = 'uncategorized'
export default function propsFromDispatch(dispatch) {
return {

View File

@ -30,7 +30,7 @@ describe('sources/api', () => {
contextType: 'group',
contextId: 123,
sortBy: {sort: 'date_added', dir: 'desc'},
searchString: ''
searchString: '',
}
let apiSource
let alertFuncSpy
@ -42,7 +42,7 @@ describe('sources/api', () => {
refreshToken: callback => {
callback('freshJWT')
},
alertFunc: alertFuncSpy
alertFunc: alertFuncSpy,
})
fetchMock.mock('/api/session', '{}')
})
@ -135,7 +135,7 @@ describe('sources/api', () => {
contextType: 'course',
contextId: '17',
sortBy: {sort: 'alphabetical', dir: 'asc'},
searchString: 'hello world'
searchString: 'hello world',
}
})
@ -214,8 +214,8 @@ describe('sources/api', () => {
bookmark: 'newBookmark',
links: [
{href: 'link1', title: 'Link 1'},
{href: 'link2', title: 'Link 2'}
]
{href: 'link2', title: 'Link 2'},
],
})
})
})
@ -289,7 +289,7 @@ describe('sources/api', () => {
it('makes a request to the folders api with the given host and ID', () => {
subject()
sinon.assert.calledWith(apiSource.apiFetch, 'about://canvas.rce/api/folders/2', {
Authorization: 'Bearer theJWT'
Authorization: 'Bearer theJWT',
})
})
@ -307,13 +307,9 @@ describe('sources/api', () => {
it('makes a request to the files api with given host and folder ID', () => {
subject()
sinon.assert.calledWith(
apiSource.apiFetch,
'https://canvas.rce/api/files/2?&category=uncategorized',
{
Authorization: 'Bearer theJWT'
}
)
sinon.assert.calledWith(apiSource.apiFetch, 'https://canvas.rce/api/files/2', {
Authorization: 'Bearer theJWT',
})
})
describe('with perPage set', () => {
@ -325,9 +321,9 @@ describe('sources/api', () => {
subject()
sinon.assert.calledWith(
apiSource.apiFetch,
'https://canvas.rce/api/files/2?per_page=50&category=uncategorized',
'https://canvas.rce/api/files/2?per_page=50',
{
Authorization: 'Bearer theJWT'
Authorization: 'Bearer theJWT',
}
)
})
@ -340,7 +336,7 @@ describe('sources/api', () => {
it('makes a request to the bookmark', () => {
subject()
sinon.assert.calledWith(apiSource.apiFetch, bookmark, {
Authorization: 'Bearer theJWT'
Authorization: 'Bearer theJWT',
})
})
})
@ -413,7 +409,7 @@ describe('sources/api', () => {
return apiSource
.fetchIconMakerFolder({
contextType: 'course',
contextId: '22'
contextId: '22',
})
.then(() => {
sinon.assert.calledWith(
@ -439,7 +435,7 @@ describe('sources/api', () => {
return apiSource
.fetchMediaFolder({
contextType: 'course',
contextId: '22'
contextId: '22',
})
.then(() => {
sinon.assert.calledWith(
@ -522,7 +518,7 @@ describe('sources/api', () => {
.then(() => {
sinon.assert.calledWith(alertFuncSpy, {
text: 'Something went wrong uploading, check your connection and try again.',
variant: 'error'
variant: 'error',
})
})
.catch(() => {
@ -552,7 +548,7 @@ describe('sources/api', () => {
.then(() => {
sinon.assert.calledWith(alertFuncSpy, {
text: 'File storage quota exceeded',
variant: 'error'
variant: 'error',
})
})
.catch(e => {})
@ -569,7 +565,7 @@ describe('sources/api', () => {
uploadUrl = 'upload-url'
preflightProps = {
upload_params: {},
upload_url: uploadUrl
upload_url: uploadUrl,
}
file = {url: 'file-url'}
fetchMock.mock(uploadUrl, file)
@ -586,7 +582,7 @@ describe('sources/api', () => {
.then(() => {
sinon.assert.calledWith(alertFuncSpy, {
text: 'Something went wrong uploading, check your connection and try again.',
variant: 'error'
variant: 'error',
})
})
.catch(() => {})
@ -646,7 +642,7 @@ describe('sources/api', () => {
const fileId = '123'
const response = {
location: `http://canvas/api/v1/files/${fileId}?foo=bar`,
uuid: 'xyzzy'
uuid: 'xyzzy',
}
fetchMock.mock(preflightProps.upload_url, response)
return apiSource.uploadFRD(fileDomObject, preflightProps).then(response => {
@ -661,7 +657,7 @@ describe('sources/api', () => {
const fileId = '1023~789'
const response = {
location: `http://canvas/api/v1/files/${fileId}?foo=bar`,
uuid: 'xyzzy'
uuid: 'xyzzy',
}
fetchMock.mock(preflightProps.upload_url, response)
return apiSource.uploadFRD(fileDomObject, preflightProps).then(response => {
@ -676,15 +672,15 @@ describe('sources/api', () => {
describe('api mapping', () => {
const body = {
bookmark: 'mo.images',
files: [{href: '/some/where', uuid: 'xyzzy'}]
files: [{href: '/some/where', uuid: 'xyzzy'}],
}
props.images = {
group: {
isLoading: false,
hasMore: true,
bookmark: null,
files: []
}
files: [],
},
}
props.searchString = 'panda'
@ -702,7 +698,7 @@ describe('sources/api', () => {
assert.deepStrictEqual(page, {
bookmark: 'mo.images',
files: [{href: '/some/where?wrap=1', uuid: 'xyzzy'}],
searchString: 'panda'
searchString: 'panda',
})
fetchMock.restore()
})
@ -716,7 +712,7 @@ describe('sources/api', () => {
assert.deepEqual(page, {
bookmark: 'mo.images',
files: [{href: '/some/where?wrap=1', uuid: 'xyzzy'}],
searchString: 'panda'
searchString: 'panda',
})
fetchMock.restore()
})
@ -753,7 +749,7 @@ describe('sources/api', () => {
const postBody = JSON.parse(fetchMock.lastOptions(uri).body)
assert.deepEqual(postBody, {
fileId,
usageRight: usageRights.usageRight
usageRight: usageRights.usageRight,
})
})
})
@ -863,7 +859,7 @@ describe('sources/api', () => {
describe('headerFor', () => {
it('returns an authorization header', () => {
assert.deepStrictEqual(headerFor('the_jwt'), {
Authorization: 'Bearer the_jwt'
Authorization: 'Bearer the_jwt',
})
})
})
@ -880,8 +876,8 @@ describe('sources/api', () => {
it('uses the windowOverride protocol if present', () => {
const win = {
location: {
protocol: 'https:'
}
protocol: 'https:',
},
}
assert.strictEqual(
originFromHost('http://host:port', win),

View File

@ -839,6 +839,12 @@ describe "Files API", type: :request do
it { is_expected.not_to include uncategorized }
end
it "returns file category with the response" do
json = api_call(:get, @files_path, @files_path_options, {})
res = json.map { |f| f["category"] }
expect(res).to eq %w[uncategorized uncategorized uncategorized]
end
describe "sort" do
it "lists files in alphabetical order" do
json = api_call(:get, @files_path, @files_path_options, {})