Filter by category when viewing saved B&I
Closes MAT-633 flag=buttons_and_icons Test Plan - Enable buttons & icons in your site admin account if needed - Build packages/canvas-rce and re-bundle Canvas JS with webpack - In a course, create more than 50 buttons & icons. this can be done programatically. Chat with Weston if you would like directions. - In that course, open the "Saved Buttons & Icons" tray by clicking the small arrow next to the buttons and icons tool. - Verify buttons and icons are loaded (50) with an option to load more at the bottom of the tray. - Click "Load More" and verify the rest of the buttons and icons are rendered. - Click one of the buttons and icons and verify it is embedded into the RCE - Focus the embedded button and icon in the RCE. Verify an "Edit" option appears in the floating toolbar. Verify you can edit the button and icon - Navigate to the files UI for the course and navigate to the Buttons and Icons folder - Select the menu button on one of the svg files (the vertical "...") - Choose "Move" and move the button and icon svg to a new folder in the course (not nested in the "Buttons and Icons" folder) - Navigate to an RCE in the course and open the "Saved Buttons & Icons" tray again. - Verify the button that is no longer in the primary "Buttons and Icons" folder still shows up and is embedable. Change-Id: I583f32d59330b8e3bf067ab606eac348882173ee Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/283795 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Jacob DeWar <jacob.dewar@instructure.com> Product-Review: David Lyons <lyons@instructure.com> Reviewed-by: Jacob DeWar <jacob.dewar@instructure.com>
This commit is contained in:
parent
700c2a59c1
commit
7f20fd23e2
|
@ -16,95 +16,39 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {func, shape, string} from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import {BTN_AND_ICON_ATTRIBUTE} from '../../instructure_buttons/registerEditToolbar'
|
||||
import Images from '../../instructure_image/Images'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import ImageList from '../../instructure_image/Images'
|
||||
import {useStoreProps} from '../../shared/StoreContext'
|
||||
import {BUTTONS_AND_ICONS} from '../registerEditToolbar'
|
||||
|
||||
export function rceToFile({createdAt, id, name, thumbnailUrl, type, url}) {
|
||||
return {
|
||||
content_type: type,
|
||||
date: createdAt,
|
||||
display_name: name,
|
||||
filename: name,
|
||||
href: url,
|
||||
id,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
[BTN_AND_ICON_ATTRIBUTE]: true
|
||||
}
|
||||
}
|
||||
|
||||
const SavedButtonList = ({context, onImageEmbed, searchString, sortBy, source}) => {
|
||||
const [buttonsAndIconsBookmark, setButtonsAndIconsBookmark] = useState(null)
|
||||
const [buttonsAndIcons, setButtonsAndIcons] = useState([])
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const resetState = () => {
|
||||
setButtonsAndIconsBookmark(null)
|
||||
setButtonsAndIcons([])
|
||||
setHasMore(true)
|
||||
setIsLoading(true)
|
||||
}
|
||||
|
||||
const onLoadedImages = ({bookmark, files}) => {
|
||||
setButtonsAndIconsBookmark(bookmark)
|
||||
setHasMore(bookmark !== null)
|
||||
setIsLoading(false)
|
||||
|
||||
setButtonsAndIcons(prevButtonsAndIcons => [
|
||||
...prevButtonsAndIcons,
|
||||
...files.filter(({type}) => type === 'image/svg+xml').map(rceToFile)
|
||||
])
|
||||
}
|
||||
|
||||
const fetchButtonsAndIcons = bookmark => {
|
||||
setIsLoading(true)
|
||||
source.fetchButtonsAndIcons(
|
||||
{contextId: context.id, contextType: context.type},
|
||||
bookmark,
|
||||
searchString,
|
||||
sortBy,
|
||||
onLoadedImages
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resetState()
|
||||
}, [searchString, sortBy.order, sortBy.sort])
|
||||
const SavedButtonList = ({onImageEmbed}) => {
|
||||
const storeProps = useStoreProps()
|
||||
const {files, bookmark, isLoading, hasMore} = storeProps.images[storeProps.contextType]
|
||||
|
||||
return (
|
||||
<Images
|
||||
contextType={context.type}
|
||||
fetchInitialImages={() => {
|
||||
fetchButtonsAndIcons()
|
||||
}}
|
||||
fetchNextImages={() => {
|
||||
fetchButtonsAndIcons(buttonsAndIconsBookmark)
|
||||
}}
|
||||
images={{[context.type]: {error: null, files: buttonsAndIcons, hasMore, isLoading}}}
|
||||
onImageEmbed={onImageEmbed}
|
||||
searchString={searchString}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
<View>
|
||||
<ImageList
|
||||
fetchInitialImages={() => storeProps.fetchInitialImages({category: BUTTONS_AND_ICONS})}
|
||||
fetchNextImages={() => storeProps.fetchNextImages({category: BUTTONS_AND_ICONS})}
|
||||
contextType={storeProps.contextType}
|
||||
images={{
|
||||
[storeProps.contextType]: {
|
||||
files,
|
||||
bookmark,
|
||||
hasMore,
|
||||
isLoading
|
||||
}
|
||||
}}
|
||||
sortBy={{
|
||||
sort: 'date_added',
|
||||
order: 'desc'
|
||||
}}
|
||||
onImageEmbed={onImageEmbed}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
SavedButtonList.propTypes = {
|
||||
context: shape({
|
||||
id: string.isRequired,
|
||||
type: string.isRequired
|
||||
}),
|
||||
onImageEmbed: func.isRequired,
|
||||
searchString: string,
|
||||
sortBy: shape({
|
||||
order: string,
|
||||
sort: string
|
||||
}),
|
||||
source: shape({
|
||||
fetchButtonsAndIcons: func.isRequired
|
||||
})
|
||||
}
|
||||
|
||||
export default SavedButtonList
|
||||
|
|
|
@ -17,127 +17,126 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {render, waitFor} from '@testing-library/react'
|
||||
import sinon from 'sinon'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import SavedButtonList from '../SavedButtonList'
|
||||
|
||||
import RceApiSource from '../../../../../rcs/api'
|
||||
jest.mock('../../../shared/StoreContext', () => {
|
||||
return {
|
||||
useStoreProps: () => ({
|
||||
images: {
|
||||
Course: {
|
||||
files: [
|
||||
{
|
||||
id: 722,
|
||||
filename: 'grid.png',
|
||||
thumbnail_url:
|
||||
'http://canvas.docker/images/thumbnails/722/E6uaQSJaQYl95XaVMnoqYU7bOlt0WepMsTB9MJ8b',
|
||||
display_name: 'image_one.png',
|
||||
href: 'http://canvas.docker/courses/21/files/722?wrap=1',
|
||||
download_url: 'http://canvas.docker/files/722/download?download_frd=1',
|
||||
content_type: 'image/png',
|
||||
published: true,
|
||||
hidden_to_user: true,
|
||||
locked_for_user: false,
|
||||
unlock_at: null,
|
||||
lock_at: null,
|
||||
date: '2021-11-03T19:21:27Z',
|
||||
uuid: 'E6uaQSJaQYl95XaVMnoqYU7bOlt0WepMsTB9MJ8b'
|
||||
},
|
||||
{
|
||||
id: 716,
|
||||
filename: '1635371359_565__0266554465.jpeg',
|
||||
thumbnail_url:
|
||||
'http://canvas.docker/images/thumbnails/716/9zLFcMIFlNPVtkTHulDGRS1bhiBg8hsL0ms6VeMt',
|
||||
display_name: 'image_two.jpg',
|
||||
href: 'http://canvas.docker/courses/21/files/716?wrap=1',
|
||||
download_url: 'http://canvas.docker/files/716/download?download_frd=1',
|
||||
content_type: 'image/jpeg',
|
||||
published: true,
|
||||
hidden_to_user: false,
|
||||
locked_for_user: false,
|
||||
unlock_at: null,
|
||||
lock_at: null,
|
||||
date: '2021-10-27T21:49:19Z',
|
||||
uuid: '9zLFcMIFlNPVtkTHulDGRS1bhiBg8hsL0ms6VeMt'
|
||||
},
|
||||
{
|
||||
id: 715,
|
||||
filename: '1635371358_548__h3zmqPb-6dw.jpg',
|
||||
thumbnail_url:
|
||||
'http://canvas.docker/images/thumbnails/715/rIlrdxCJ1h5Ff18Y4C6KJf7HIvCDn5ZAbtnVpNcw',
|
||||
display_name: 'image_three.jpg',
|
||||
href: 'http://canvas.docker/courses/21/files/715?wrap=1',
|
||||
download_url: 'http://canvas.docker/files/715/download?download_frd=1',
|
||||
content_type: 'image/jpeg',
|
||||
published: true,
|
||||
hidden_to_user: false,
|
||||
locked_for_user: false,
|
||||
unlock_at: null,
|
||||
lock_at: null,
|
||||
date: '2021-10-27T21:49:18Z',
|
||||
uuid: 'rIlrdxCJ1h5Ff18Y4C6KJf7HIvCDn5ZAbtnVpNcw'
|
||||
}
|
||||
],
|
||||
bookmark: 'bookmark',
|
||||
isLoading: false,
|
||||
hasMore: false
|
||||
}
|
||||
},
|
||||
contextType: 'Course',
|
||||
fetchInitialImages: jest.fn(),
|
||||
fetchNextImages: jest.fn()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
import SavedButtonList, {rceToFile} from '../SavedButtonList'
|
||||
|
||||
describe('RCE "Buttons and Icons" Plugin > SavedButtonList', () => {
|
||||
let defaultProps, fetchPageStub, globalFetchStub
|
||||
|
||||
const apiSource = new RceApiSource({
|
||||
alertFunc: () => {},
|
||||
jwt: 'theJWT'
|
||||
})
|
||||
describe('SavedButtonList()', () => {
|
||||
let props
|
||||
const subject = () => render(<SavedButtonList {...props} />)
|
||||
|
||||
beforeEach(() => {
|
||||
globalFetchStub = sinon.stub(global, 'fetch')
|
||||
const context = {id: '101', type: 'course'}
|
||||
const buttonsAndIconsFolder = {filesUrl: 'http://rce.example.com/api/folders/52', id: '1'}
|
||||
const buttonAndIcon = {
|
||||
createdAt: '',
|
||||
id: 1,
|
||||
name: 'button.svg',
|
||||
thumbnailUrl: '',
|
||||
type: 'image/svg+xml',
|
||||
url: ''
|
||||
}
|
||||
const otherImage = {
|
||||
createdAt: '',
|
||||
id: 2,
|
||||
name: 'screenshot.jpg',
|
||||
thumbnailUrl: '',
|
||||
type: 'image/jpeg',
|
||||
url: ''
|
||||
}
|
||||
const folders = [buttonsAndIconsFolder]
|
||||
|
||||
fetchPageStub = sinon.stub(apiSource, 'fetchPage')
|
||||
fetchPageStub
|
||||
.withArgs(`/api/folders/buttons_and_icons?contextType=course&contextId=${context.id}`)
|
||||
.returns(Promise.resolve({folders}))
|
||||
|
||||
fetchPageStub
|
||||
.withArgs(
|
||||
'http://rce.example.com/api/folders/52?per_page=25&sort=created_at&order=desc',
|
||||
'theJWT'
|
||||
)
|
||||
.returns(Promise.resolve({bookmark: '', files: [buttonAndIcon, otherImage]}))
|
||||
|
||||
defaultProps = {
|
||||
context,
|
||||
onImageEmbed: () => {},
|
||||
searchString: '',
|
||||
sortBy: {order: 'desc', sort: 'date_added'},
|
||||
source: apiSource
|
||||
props = {
|
||||
onImageEmbed: jest.fn()
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fetchPageStub.restore()
|
||||
globalFetchStub.restore()
|
||||
afterEach(() => jest.clearAllMocks())
|
||||
|
||||
it('renders the image list', () => {
|
||||
const {getByTitle} = subject()
|
||||
|
||||
expect(getByTitle('Click to embed image_one.png')).toBeInTheDocument()
|
||||
expect(getByTitle('Click to embed image_two.jpg')).toBeInTheDocument()
|
||||
expect(getByTitle('Click to embed image_three.jpg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const renderComponent = componentProps => {
|
||||
return render(<SavedButtonList {...defaultProps} {...componentProps} />)
|
||||
}
|
||||
describe('when an image is clicked', () => {
|
||||
beforeEach(() => {
|
||||
const {getByTitle} = subject()
|
||||
|
||||
it('loads and displays svgs', async () => {
|
||||
const {getByAltText} = renderComponent()
|
||||
// Click the first image
|
||||
fireEvent.click(getByTitle('Click to embed image_one.png'))
|
||||
})
|
||||
|
||||
await waitFor(() => expect(getByAltText('button.svg')).toBeInTheDocument())
|
||||
})
|
||||
|
||||
it('ignores non-svg files', async () => {
|
||||
const {queryByAltText} = renderComponent()
|
||||
|
||||
await waitFor(() => queryByAltText('button.svg') != null)
|
||||
|
||||
expect(queryByAltText('screenshot.jpg')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('rceToFile', () => {
|
||||
const rceFile = {
|
||||
createdAt: '2021-08-12T18:30:53Z',
|
||||
id: '101',
|
||||
name: 'kitten.gif',
|
||||
thumbnailUrl: 'http://example.com/kitten.png',
|
||||
type: 'image/gif',
|
||||
url: 'http://example.com/kitten.gif'
|
||||
}
|
||||
|
||||
it('returns an object with type as content_type', () => {
|
||||
expect(rceToFile(rceFile).content_type).toStrictEqual('image/gif')
|
||||
})
|
||||
|
||||
it('returns an object with createdAt as date', () => {
|
||||
expect(rceToFile(rceFile).date).toStrictEqual('2021-08-12T18:30:53Z')
|
||||
})
|
||||
|
||||
it('returns an object with name as display_name', () => {
|
||||
expect(rceToFile(rceFile).display_name).toStrictEqual('kitten.gif')
|
||||
})
|
||||
|
||||
it('returns an object with name as filename', () => {
|
||||
expect(rceToFile(rceFile).filename).toStrictEqual('kitten.gif')
|
||||
})
|
||||
|
||||
it('returns an object with url as href', () => {
|
||||
expect(rceToFile(rceFile).href).toStrictEqual('http://example.com/kitten.gif')
|
||||
})
|
||||
|
||||
it('returns an object with id as id', () => {
|
||||
expect(rceToFile(rceFile).id).toStrictEqual('101')
|
||||
})
|
||||
|
||||
it('returns an object with thumbnailUrl as thumbnail_url', () => {
|
||||
expect(rceToFile(rceFile).thumbnail_url).toStrictEqual('http://example.com/kitten.png')
|
||||
})
|
||||
|
||||
it('returns an object with the buttons/icons attr set to true', () => {
|
||||
expect(rceToFile(rceFile)['data-inst-buttons-and-icons']).toEqual(true)
|
||||
it('dispatches a "loading" action', () => {
|
||||
expect(props.onImageEmbed.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"content_type": "image/png",
|
||||
"date": "2021-11-03T19:21:27Z",
|
||||
"display_name": "image_one.png",
|
||||
"download_url": "http://canvas.docker/files/722/download?download_frd=1",
|
||||
"filename": "grid.png",
|
||||
"hidden_to_user": true,
|
||||
"href": "http://canvas.docker/courses/21/files/722?wrap=1",
|
||||
"id": 722,
|
||||
"lock_at": null,
|
||||
"locked_for_user": false,
|
||||
"published": true,
|
||||
"thumbnail_url": "http://canvas.docker/images/thumbnails/722/E6uaQSJaQYl95XaVMnoqYU7bOlt0WepMsTB9MJ8b",
|
||||
"unlock_at": null,
|
||||
"uuid": "E6uaQSJaQYl95XaVMnoqYU7bOlt0WepMsTB9MJ8b",
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -22,8 +22,9 @@ const BUTTON_ID = 'inst-button-and-icons-edit'
|
|||
const TOOLBAR_ID = 'inst-button-and-icons-edit-toolbar'
|
||||
|
||||
export const BTN_AND_ICON_ATTRIBUTE = 'data-inst-buttons-and-icons'
|
||||
export const BUTTONS_AND_ICONS = 'buttons_and_icons'
|
||||
|
||||
export const shouldShowEditButton = (node) => !!node?.getAttribute(BTN_AND_ICON_ATTRIBUTE)
|
||||
export const shouldShowEditButton = node => !!node?.getAttribute(BTN_AND_ICON_ATTRIBUTE)
|
||||
|
||||
export default function registerEditToolbar(editor, onAction) {
|
||||
addButton(editor, onAction)
|
||||
|
|
|
@ -30,6 +30,7 @@ import formatMessage from '../../../format-message'
|
|||
import Filter, {useFilterSettings} from './Filter'
|
||||
import {StoreProvider} from './StoreContext'
|
||||
import {getTrayHeight} from './trayUtils'
|
||||
import {BUTTONS_AND_ICONS} from '../instructure_buttons/registerEditToolbar'
|
||||
|
||||
/**
|
||||
* Returns the translated tray label
|
||||
|
@ -46,7 +47,7 @@ function getTrayLabel(contentType, contentSubtype, contextType) {
|
|||
}
|
||||
|
||||
switch (contentSubtype) {
|
||||
case 'buttons_and_icons':
|
||||
case BUTTONS_AND_ICONS:
|
||||
return formatMessage('Buttons and Icons')
|
||||
case 'images':
|
||||
if (contentType === 'course_files') return formatMessage('Course Images')
|
||||
|
@ -188,7 +189,7 @@ const FILTER_SETTINGS_BY_PLUGIN = {
|
|||
list_buttons_and_icons: {
|
||||
contextType: 'course',
|
||||
contentType: 'course_files',
|
||||
contentSubtype: 'buttons_and_icons',
|
||||
contentSubtype: BUTTONS_AND_ICONS,
|
||||
sortValue: 'date_added',
|
||||
sortDir: 'desc',
|
||||
searchString: ''
|
||||
|
|
|
@ -25,6 +25,7 @@ import {TextInput} from '@instructure/ui-text-input'
|
|||
import {SimpleSelect} from '@instructure/ui-simple-select'
|
||||
import {IconButton} from '@instructure/ui-buttons'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {BUTTONS_AND_ICONS} from '../instructure_buttons/registerEditToolbar'
|
||||
import {
|
||||
IconLinkLine,
|
||||
IconFolderLine,
|
||||
|
@ -100,7 +101,7 @@ function renderTypeOptions(contentType, contentSubtype, userContextType) {
|
|||
}
|
||||
|
||||
// Buttons and Icons are only stored in course folders.
|
||||
if (contentSubtype !== 'buttons_and_icons') {
|
||||
if (contentSubtype !== BUTTONS_AND_ICONS) {
|
||||
options.push(
|
||||
<SimpleSelect.Option
|
||||
key="user_files"
|
||||
|
@ -231,7 +232,7 @@ export default function Filter(props) {
|
|||
// when flipped to All, the context needs to be user
|
||||
// so we can get media_objects, which are all returned in the user context
|
||||
changed.contentType = 'user_files'
|
||||
} else if (changed.contentSubtype === 'buttons_and_icons') {
|
||||
} else if (changed.contentSubtype === BUTTONS_AND_ICONS) {
|
||||
// Buttons and Icons only belong to Courses.
|
||||
changed.contentType = 'course_files'
|
||||
}
|
||||
|
|
|
@ -302,34 +302,6 @@ class RceApiSource {
|
|||
return this.fetchPage(uri)
|
||||
}
|
||||
|
||||
fetchButtonsAndIcons(
|
||||
{contextId, contextType},
|
||||
bookmark = null,
|
||||
searchString = null,
|
||||
sortBy,
|
||||
onSuccess
|
||||
) {
|
||||
const onSuccessWithFixedFileData = data => {
|
||||
onSuccess({
|
||||
...data,
|
||||
files: data.files.map(file => fixupFileUrl(contextType, contextId, file))
|
||||
})
|
||||
}
|
||||
|
||||
if (bookmark) {
|
||||
this.fetchFilesForFolder(null, bookmark).then(onSuccessWithFixedFileData)
|
||||
} else {
|
||||
this.fetchButtonsAndIconsFolder({contextId, contextType}).then(({folders}) => {
|
||||
this.fetchFilesForFolder({
|
||||
filesUrl: folders[0].filesUrl,
|
||||
perPage: 25,
|
||||
searchString,
|
||||
sortBy
|
||||
}).then(onSuccessWithFixedFileData)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fetchMediaFolder(props) {
|
||||
let uri
|
||||
if (props.contextType === 'user') {
|
||||
|
@ -347,6 +319,7 @@ class RceApiSource {
|
|||
fetchImages(props) {
|
||||
const images = props.images[props.contextType]
|
||||
const uri = images.bookmark || this.uriFor('images', props)
|
||||
|
||||
const headers = headerFor(this.jwt)
|
||||
return this.apiFetch(uri, headers).then(({bookmark, files}) => {
|
||||
return {
|
||||
|
@ -471,11 +444,13 @@ class RceApiSource {
|
|||
if (!this.hasSession) {
|
||||
await this.getSession()
|
||||
}
|
||||
|
||||
return this.apiReallyFetch(uri, headers, options)
|
||||
}
|
||||
|
||||
apiReallyFetch(uri, headers, options = {}) {
|
||||
uri = this.normalizeUriProtocol(uri)
|
||||
|
||||
return fetch(uri, {headers})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
|
|
|
@ -735,8 +735,6 @@ export function initializeMedia(_props) {
|
|||
}
|
||||
}
|
||||
|
||||
export function fetchButtonsAndIcons() {}
|
||||
|
||||
export function fetchFolders() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
BUTTONS_AND_ICONS,
|
||||
BTN_AND_ICON_ATTRIBUTE
|
||||
} from '../../rce/plugins/instructure_buttons/registerEditToolbar'
|
||||
|
||||
export const ADD_IMAGE = 'action.images.add_image'
|
||||
export const REQUEST_INITIAL_IMAGES = 'action.images.request_initial_images'
|
||||
export const REQUEST_IMAGES = 'action.images.request_images'
|
||||
|
@ -46,15 +51,29 @@ export function requestImages(contextType) {
|
|||
return {type: REQUEST_IMAGES, payload: {contextType}}
|
||||
}
|
||||
|
||||
export function receiveImages({response, contextType}) {
|
||||
export function receiveImages({response, contextType, opts = {}}) {
|
||||
const {files, bookmark, searchString} = response
|
||||
return {type: RECEIVE_IMAGES, payload: {files, bookmark, contextType, searchString}}
|
||||
|
||||
return {
|
||||
type: RECEIVE_IMAGES,
|
||||
payload: {files: files.map(f => applyAttributes(f, opts)), bookmark, contextType, searchString}
|
||||
}
|
||||
}
|
||||
|
||||
export function failImagesLoad({error, contextType}) {
|
||||
return {type: FAIL_IMAGES_LOAD, payload: {error, contextType}}
|
||||
}
|
||||
|
||||
export const applyAttributes = (file, opts) => {
|
||||
const augmentedFile = {...file}
|
||||
|
||||
if (opts.category === BUTTONS_AND_ICONS) {
|
||||
augmentedFile[BTN_AND_ICON_ATTRIBUTE] = true
|
||||
}
|
||||
|
||||
return augmentedFile
|
||||
}
|
||||
|
||||
// dispatches the start of the load, requests a page for the collection from
|
||||
// the source, then dispatches the loaded page to the store on success or
|
||||
// clears the load on failure
|
||||
|
@ -65,7 +84,7 @@ export function fetchImages(opts = {}) {
|
|||
const state = getState()
|
||||
return state.source
|
||||
.fetchImages({...state, category})
|
||||
.then(response => dispatch(receiveImages({response, contextType: state.contextType})))
|
||||
.then(response => dispatch(receiveImages({response, contextType: state.contextType, opts})))
|
||||
.catch(error => dispatch(failImagesLoad({error, contextType: state.contextType})))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import {fileEmbed} from '../../common/mimeClass'
|
|||
import {isPreviewable} from '../../rce/plugins/shared/Previewable'
|
||||
import {isImage, isAudioOrVideo} from '../../rce/plugins/shared/fileTypeUtils'
|
||||
import {fixupFileUrl} from '../../common/fileUrl'
|
||||
import {BUTTONS_AND_ICONS} from '../../rce/plugins/instructure_buttons/registerEditToolbar'
|
||||
|
||||
export const COMPLETE_FILE_UPLOAD = 'COMPLETE_FILE_UPLOAD'
|
||||
export const FAIL_FILE_UPLOAD = 'FAIL_FILE_UPLOAD'
|
||||
|
@ -41,8 +42,6 @@ export const STOP_LOADING = 'STOP_LOADING'
|
|||
export const STOP_MEDIA_UPLOADING = 'STOP_MEDIA_UPLOADING'
|
||||
export const TOGGLE_UPLOAD_FORM = 'TOGGLE_UPLOAD_FORM'
|
||||
|
||||
export const BUTTONS_AND_ICONS = 'buttons_and_icons'
|
||||
|
||||
export function startLoading() {
|
||||
return {type: START_LOADING}
|
||||
}
|
||||
|
|
|
@ -418,112 +418,6 @@ describe('sources/api', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('fetchButtonsAndIcons', () => {
|
||||
const props = {contextId: '1', contextType: 'course'}
|
||||
|
||||
beforeEach(() => {
|
||||
const fetchPageResponseBody = {
|
||||
bookmark: 'http://example.com/?p=3',
|
||||
files: [
|
||||
{
|
||||
id: '101',
|
||||
name: 'button.svg',
|
||||
thumbnailUrl: '/files/1/download',
|
||||
type: 'image/svg+xml',
|
||||
url: '/files/1/download/'
|
||||
}
|
||||
]
|
||||
}
|
||||
sinon.stub(apiSource, 'fetchPage').returns(Promise.resolve(fetchPageResponseBody))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
apiSource.fetchPage.restore()
|
||||
})
|
||||
|
||||
describe('when a bookmark is present', () => {
|
||||
it('fetches with the bookmark', () => {
|
||||
apiSource.fetchButtonsAndIcons(props, 'http://example.com/?p=2', () => {})
|
||||
sinon.assert.calledWith(apiSource.fetchPage, 'http://example.com/?p=2')
|
||||
})
|
||||
|
||||
it('calls the onSuccess arg with the returned bookmark', () => {
|
||||
const onSuccess = ({bookmark}) => {
|
||||
assert.strictEqual(bookmark, fetchPageResponseBody.bookmark)
|
||||
}
|
||||
|
||||
apiSource.fetchButtonsAndIcons(props, 'http://example.com/?p=2', onSuccess)
|
||||
})
|
||||
|
||||
it('calls the onSuccess arg with the returned files', () => {
|
||||
const onSuccess = ({files}) => {
|
||||
assert.deepEqual(files, [
|
||||
{
|
||||
id: '101',
|
||||
name: 'button.svg',
|
||||
thumbnailUrl: '/files/1/download',
|
||||
type: 'image/svg+xml',
|
||||
url: '/courses/1/files/1?wrap=1' // url is normalized to include the context
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
apiSource.fetchButtonsAndIcons(props, 'http://example.com/?p=2', onSuccess)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a bookmark is not present', () => {
|
||||
const filesUrl = 'http://example.com'
|
||||
const folderResponseBody = {
|
||||
bookmark: 'http://example.com/?p=2',
|
||||
folders: [{filesUrl, id: 24}]
|
||||
}
|
||||
|
||||
let fetchButtonsAndIconsFolderPromise
|
||||
|
||||
beforeEach(() => {
|
||||
fetchButtonsAndIconsFolderPromise = Promise.resolve(folderResponseBody)
|
||||
sinon
|
||||
.stub(apiSource, 'fetchButtonsAndIconsFolder')
|
||||
.returns(fetchButtonsAndIconsFolderPromise)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
apiSource.fetchButtonsAndIconsFolder.restore()
|
||||
})
|
||||
|
||||
it('fetches the buttons and icons folder, then fetches the files within that folder', async () => {
|
||||
apiSource.fetchButtonsAndIcons(props, null, () => {})
|
||||
await fetchButtonsAndIconsFolderPromise
|
||||
sinon.assert.calledWith(apiSource.fetchPage, `${filesUrl}?per_page=25`)
|
||||
})
|
||||
|
||||
it('calls the onSuccess arg with the returned bookmark', () => {
|
||||
const onSuccess = ({bookmark}) => {
|
||||
assert.strictEqual(bookmark, folderResponseBody.bookmark)
|
||||
}
|
||||
|
||||
apiSource.fetchButtonsAndIcons(props, null, onSuccess)
|
||||
})
|
||||
|
||||
it('calls the onSuccess arg with the returned files', () => {
|
||||
const onSuccess = ({files}) => {
|
||||
assert.deepEqual(files, [
|
||||
{
|
||||
id: '101',
|
||||
name: 'button.svg',
|
||||
thumbnailUrl: '/files/1/download',
|
||||
type: 'image/svg+xml',
|
||||
url: '/courses/1/files/1?wrap=1' // url is normalized to include the context
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
apiSource.fetchButtonsAndIcons(props, null, onSuccess)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchMediaFolder', () => {
|
||||
let files
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -58,6 +58,31 @@ describe('Image dispatch shapes', () => {
|
|||
assert(payload.searchString === 'panda')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the "category" is set to "buttons_and_icons', () => {
|
||||
let buttonAndIconsResponse, opts
|
||||
|
||||
const subject = () => actions.receiveImages(buttonAndIconsResponse)
|
||||
|
||||
beforeEach(() => {
|
||||
buttonAndIconsResponse = {
|
||||
response: {
|
||||
files: [{id: 1}, {id: 2}, {id: 3}]
|
||||
},
|
||||
contextType,
|
||||
opts: {
|
||||
category: 'buttons_and_icons'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('applies the buttons and icons attribute to each file', () => {
|
||||
assert.deepEqual(
|
||||
subject().payload.files.map(f => f['data-inst-buttons-and-icons']),
|
||||
[true, true, true]
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue