UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

399 lines (398 loc) 12.4 kB
// Copyright (C) 2017 - 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 formatMessage from '../format-message'; import _ from 'lodash'; import minimatch from 'minimatch'; import { TreeBrowser } from '@instructure/ui-tree-browser'; import { Text } from '@instructure/ui-text'; import { Spinner } from '@instructure/ui-spinner'; import { IconOpenFolderSolid, IconFolderSolid, IconImageLine } from '@instructure/ui-icons'; import PropTypes from 'prop-types'; import { getIconFromType, isImage } from '../rce/plugins/shared/fileTypeUtils'; import { showFlashError } from '../common/FlashAlert'; import natcompare from '../common/natcompare'; export const PENDING_MEDIA_ENTRY_ID = 'maybe'; class FileBrowser extends React.Component { constructor(_props2) { super(_props2); // Memoized function to fetch all subfolders // of the given folder ID, handing pagination this.fetchSubFolders = _.memoize(id => { this.source.fetchBookmarkedData(this.source.fetchSubFolders.bind(this.source), { folderId: id, perPage: 50 }, result => { this.populateCollectionsList(result.folders); }, error => { this.props.onLoading(false); console.error('Error fetching data from API'); console.error(error); }); }); this.populateCollectionsList = (folderList, opts = {}) => { this.setState((state, props) => { const newCollections = _.cloneDeep(state.collections); folderList.forEach(folder => { const collection = this.formatFolderInfo(folder, { ...opts, searchString: props.searchString }); newCollections[collection.id] = collection; const parentId = folder.parentId || 0; const collectionCollections = newCollections[parentId].collections; if (!collectionCollections.includes(collection.id)) { collectionCollections.push(collection.id); newCollections[parentId].collections = this.orderedIdsFromList(newCollections, collectionCollections); } }); return { collections: newCollections }; }); }; this.populateItemsList = fileList => { this.setState((state, _props) => { const newItems = _.cloneDeep(state.items); const newCollections = _.cloneDeep(state.collections); fileList.forEach(file => { if (this.contentTypeIsAllowed(file.type)) { const item = this.formatFileInfo(file); newItems[item.id] = item; const folder_id = file.folderId; const collectionItems = newCollections[folder_id].items; if (!collectionItems.includes(item.id)) { collectionItems.push(item.id); newCollections[folder_id].items = this.orderedIdsFromList(newItems, collectionItems); } } }); return { items: newItems, collections: newCollections }; }); }; this.onFolderToggle = folder => { const folderId = folder.id; this.setState((state, _props) => { const collection = state.collections[folderId]; let newFolders = []; let newCollections = state.collections; const { openFolders } = state; if (!collection.locked && openFolders.includes(folderId)) { newFolders = newFolders.concat(openFolders.filter(id => id !== folderId)); } else if (!collection.locked) { newFolders = newFolders.concat(openFolders); newFolders.push(folderId); newCollections = _.cloneDeep(state.collections); newCollections[folderId] = collection; } return { openFolders: newFolders, uploadFolder: folderId, collections: newCollections }; }, () => { if (this.state.openFolders.includes(folderId)) { const collection = this.state.collections[folderId]; if (!collection.locked) { this.getFolderData(folderId); } } }); }; this.onFileClick = file => { const selectedItem = this.state.items[file.id]; if (selectedItem.isDisabled) return; this.props.selectFile(selectedItem); }; this.setFailureMessage = message => { showFlashError(message)(); }; this.state = { collections: { 0: { id: 0, collections: [] } }, items: {}, openFolders: [], loadingCount: 0 }; this.source = _props2.source; this.updatePropsWithThumbnailOrIcon = this.updatePropsWithThumbnailOrIcon.bind(this); } componentDidMount() { this.getRootFolders(); } componentDidUpdate() { this.state.openFolders.forEach(fid => { if (this.props.searchString !== this.state.collections[fid].searchString) { this.getFolderData(fid); } }); } getContextName(contextType) { if (contextType === 'course') { return formatMessage('Course files'); } else { return formatMessage('Group files'); } } getRootFolders() { if (this.props.useContextAssets) { this.getContextFolders(); } this.getUserFolders(); } getUserFolders() { this.getRootFolderData('user', 'self', { name: formatMessage('My files') }); } getContextFolders() { const { type, id } = this.props.context; if (type && id) { this.getRootFolderData(type, id, { name: this.getContextName(type) }); } } increaseLoadingCount() { let { loadingCount } = this.state; loadingCount += 1; this.setState({ loadingCount }); } decreaseLoadingCount() { let { loadingCount } = this.state; loadingCount -= 1; this.setState({ loadingCount }); } getRootFolderData(context, contextId, opts = {}) { this.increaseLoadingCount(); this.source.fetchRootFolder({ contextType: context, contextId }).then(result => { this.populateRootFolder(result.folders[0], opts); }).catch(error => { this.decreaseLoadingCount(); if (error.response && error.response.status !== 401) { this.setFailureMessage(formatMessage('Something went wrong')); } }); } populateRootFolder(data, opts = {}) { this.decreaseLoadingCount(); this.populateCollectionsList([data], opts); this.getFolderData(data.id); } fetchFiles(id) { this.source.fetchBookmarkedData(this.source.fetchFilesForFolder.bind(this.source), { searchString: this.props.searchString, perPage: 50, filesUrl: this.state.collections[id]?.api?.filesUrl }, result => { this.populateItemsList(result.files); }, error => { this.props.onLoading(false); console.error(error); }); } getFolderData(id) { if (!this.state.collections[id].locked) { this.setState((state, _props) => { const collections = { ...state.collections }; const collection = { ...collections[id] }; collection.items = []; collection.searchString = this.props.searchString; collections[id] = collection; return { collections }; }, () => { this.fetchSubFolders(id); this.fetchFiles(id); }); } } contentTypeIsAllowed(contentType) { for (const pattern of this.props.contentTypes) { if (minimatch(contentType, pattern)) { return true; } } return false; } formatFolderInfo(apiFolder, opts = {}) { const descriptor = apiFolder.lockedForUser ? formatMessage('Locked') : null; const folder = { api: apiFolder, id: apiFolder.id, collections: [], items: [], name: apiFolder.name, context: `/${apiFolder.contextType?.toLowerCase()}s/${apiFolder.contextId}`, canUpload: apiFolder.canUpload, locked: apiFolder.lockedForUser, descriptor, ...opts }; const existingCollections = this.state.collections[apiFolder.id]; Object.assign(folder, existingCollections && { collections: existingCollections.collections, items: existingCollections.items }); return folder; } updatePropsWithThumbnailOrIcon(props) { const { id } = props; const file = this.state.items[id].api; let thumbnail, itemIcon; if (isImage(file.type)) { if (file.thumbnailUrl) { thumbnail = file.thumbnailUrl; } else { itemIcon = IconImageLine; } } else { itemIcon = getIconFromType(file.type); } const containerRef = node => { if (node && !node.title && props.name) node.title = props.name; }; return { ...props, thumbnail, itemIcon, containerRef }; } formatFileInfo(apiFile, opts = {}) { const { collections } = this.state; const context = collections[apiFile.folderId].context; const isMediaPending = apiFile.mediaEntryId === PENDING_MEDIA_ENTRY_ID; const file = { api: apiFile, id: apiFile.id, name: apiFile.name, isDisabled: isMediaPending, src: `${context}/files/${apiFile.id}/preview${context.includes('user') ? `?verifier=${apiFile.uuid}` : ''}`, alt: apiFile.name, ...opts }; if (apiFile.iframeUrl) { // it's a media_object file.src = apiFile.iframeUrl; } if (isMediaPending) { file.descriptor = formatMessage('Media file is processing. Please try again later.'); } return file; } orderedIdsFromList(list, ids) { try { const sortedIds = ids.sort((a, b) => natcompare.strings(list[a].name, list[b].name)); return sortedIds; } catch (error) { console.error(error); return ids; } } findFolderForFile(file) { const { collections } = this.state; const folderKey = Object.keys(collections).find(key => { const items = collections[key].items; return items && items.includes(file.id); }); return collections[folderKey]; } renderLoading() { if (this.state.loadingCount > 0) { return /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading folders'), size: "small" }); } else { return null; } } render() { const element = /*#__PURE__*/React.createElement("div", { className: "file-browser__container" }, /*#__PURE__*/React.createElement(Text, null, formatMessage('Available folders')), /*#__PURE__*/React.createElement("div", { className: "file-browser__tree" }, /*#__PURE__*/React.createElement(TreeBrowser, { collections: this.state.collections, items: this.state.items, size: "medium", onCollectionToggle: this.onFolderToggle, onItemClick: this.onFileClick, treeLabel: formatMessage('Folder tree'), rootId: 0, showRootCollection: false, expanded: this.state.openFolders, collectionIconExpanded: IconOpenFolderSolid, collectionIcon: IconFolderSolid, selectionType: "single", getItemProps: this.updatePropsWithThumbnailOrIcon }), this.renderLoading())); return element; } } FileBrowser.propTypes = { allowUpload: PropTypes.bool, selectFile: PropTypes.func.isRequired, contentTypes: PropTypes.arrayOf(PropTypes.string), useContextAssets: PropTypes.bool, searchString: PropTypes.string, onLoading: PropTypes.func.isRequired, context: PropTypes.shape({ type: PropTypes.string.isRequired, id: PropTypes.string.isRequired }).isRequired }; FileBrowser.defaultProps = { allowUpload: true, contentTypes: ['*/*'], useContextAssets: true }; export default FileBrowser;