326 lines
9.6 KiB
JavaScript
Raw Normal View History

import React from 'react';
import { connect } from 'react-redux';
2018-07-06 18:56:28 -04:00
import { orderBy, map } from 'lodash';
import { Map } from 'immutable';
import fuzzy from 'fuzzy';
2018-07-17 19:13:52 -04:00
import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
import {
loadMedia as loadMediaAction,
persistMedia as persistMediaAction,
deleteMedia as deleteMediaAction,
insertMedia as insertMediaAction,
loadMediaDisplayURL as loadMediaDisplayURLAction,
closeMediaLibrary as closeMediaLibraryAction,
WIP - Global UI (#785) * update top bar and collections sidebar UI * update collection entries UI * improve global layout * merge search page into collection page * enable new entry button * search fixup * wip -initial editor update * update editor scrolling and markdown toolbar position * wip * editor toolbar progress * editor toolbar wip * finished basic editor toolbar * add standalone toggle component * improve markdown toolbar spacing * add user avatar placeholder * finish markdown toggle styling * refactor icon setup, add new icons * add new icons to markdown editor toolbar * remove extra app container * add markdown active mark style * relation and text widget styling * widget design updates, basic list/object design update * widget style updates, image widget improvements * refactor widget directory, fix file removal * widget focus styles * finish editor widget focus styles * migrate media library modal to react-modal * wip - migrate editor component form to modal * wip - move editor component form to modal * wip - embed plugin forms in the editor * inline shortcode forms working * disable react hot loading, its breaking things * improve shortcode form styles * make shortcode form collapsible, improve styling * add close functionality to shortcode blocks * improve base media library styling * fix shortcode label * migrate unstyled workflow to new UI * wip - reorganizing everything * more work moving everything * finish more moving and eliminating stuff * restructure, remove react-toolbox * wip - removing old stuff, more restructure * finish restructure * wip - css arch * switch back to test repo * update react-datetime to ^2.11.0 * remove leftover react-toolbox button * more restructuring clean-up * fix UI component directory case * wip -css editor control style * wip - consolidate widget styles * wip - use a single control renderer * fixed object values breaking * wip - editor control active styles * pass control wrapper to widgets * ensure branch name is trimmed * wip - improve widget authoring support * import Map to Widget component * refactor toolbar buttons * wip - more widget active styles * break out editor toggle component * add local scroll sync back * update editor toggle icons * limit editor control pane content width * fix editor control spacing * migrate markdown toolbar stickiness to css * fix markdown toolbar border radius * temporarily use test backend * stop markdown toolbar from going to bottom * restore disabled markdown toolbar buttons for raw * test markdown widget without focus styles * more widget updates * remove card visuals from editor * disable dragging editor split off screen * use editorControl component for shortcode fields * make header site link configurable * add configurable collection descriptions * temporarily add example assets * add basic list view * remove outdated css mixins * add and implement search icon * activate quick add menu * visualize usable space in editor view * fix entry close, other improvements * wip - editorial workflow updates * some dropshadow and other CSS tweaks * workflow ui updates * add worfklow card buttons * fix workflow card button handlers * some dropshadow and other CSS tweaks * make workflow board wider * center workflow and collection views * add basic responsiveness * a bunch of fun UI fixes! a BUNCH! (#875) * give `.nc-entryEditor-toolbar-mainSection` left and right child divs * a bunch of fun UI fixes! a BUNCH! * remove obscure --buttonShadow * revert to test repo * fix not found page styling * allow workflow publishing from any column * disallow publishing from all columns, with feedback * fix new entry button * fix markdown state persisting across entries * enable simple workflow save and new from editor * update slug in address bar when saving new entry * wip - workflow updates, deletion working * add status change functionality to editor * wip - improving status change from editor * editor toolbar back button improvements, loading improvements, cleanup * progress on the media library UI cleanup * remove font smothing css * a quick fix for these buttons * tweaks * progress on media library modal— broken FYI * fix media library functionality, finish migrating footer * remove media library footer files * remove leftover css import * fix media library * editor publishing functionality complete (unstyled) * remove leftover loader var from media library * wip - editor publishing styles * add status dropdown styling * editor toolbar style updates * editor toolbar state improvements * progress on the media library UI cleanup, style improvements * finish editorial workflow editor styling * finish media library styling * fix config * add what-input to optimize focus styling * fix button * fix navigation blocking for simple workflow * improve simple workflow publishing * add avatar dropdown to editor top bar * style github and test-repo auth pages * add git gateway auth page styles * improve editor error styling
2017-12-07 12:37:10 -05:00
} from 'Actions/mediaLibrary';
2018-07-06 18:56:28 -04:00
import MediaLibraryModal from './MediaLibraryModal';
/**
* Extensions used to determine which files to show when the media library is
* accessed from an image insertion field.
*/
const IMAGE_EXTENSIONS_VIEWABLE = ['jpg', 'jpeg', 'webp', 'gif', 'png', 'bmp', 'tiff', 'svg'];
const IMAGE_EXTENSIONS = [...IMAGE_EXTENSIONS_VIEWABLE];
class MediaLibrary extends React.Component {
/**
* The currently selected file and query are tracked in component state as
* they do not impact the rest of the application.
*/
state = {
selectedFile: {},
query: '',
};
componentDidMount() {
this.props.loadMedia();
}
UNSAFE_componentWillReceiveProps(nextProps) {
/**
* We clear old state from the media library when it's being re-opened
* because, when doing so on close, the state is cleared while the media
* library is still fading away.
*/
const isOpening = !this.props.isVisible && nextProps.isVisible;
if (isOpening) {
this.setState({ selectedFile: {}, query: '' });
}
}
componentDidUpdate(prevProps) {
const isOpening = !prevProps.isVisible && this.props.isVisible;
if (isOpening && prevProps.privateUpload !== this.props.privateUpload) {
this.props.loadMedia({ privateUpload: this.props.privateUpload });
}
}
getDisplayURL = file => {
const { isVisible, loadMediaDisplayURL, displayURLs } = this.props;
if (!isVisible) {
return '';
}
if (file && file.url) {
return file.url;
}
const { url, isFetching } = displayURLs.get(file.id, Map()).toObject();
if (url && url !== '') {
return url;
}
if (!isFetching) {
loadMediaDisplayURL(file);
}
return '';
};
/**
* Filter an array of file data to include only images.
*/
2017-11-13 10:58:20 -05:00
filterImages = (files = []) => {
return files.filter(file => {
const ext = fileExtension(file.name).toLowerCase();
return IMAGE_EXTENSIONS.includes(ext);
});
};
/**
* Transform file data for table display.
*/
toTableData = files => {
const tableData =
files &&
files.map(({ key, name, id, size, queryOrder, url, urlIsPublicPath, getBlobPromise }) => {
const ext = fileExtension(name).toLowerCase();
return {
key,
id,
name,
type: ext.toUpperCase(),
size,
queryOrder,
url,
urlIsPublicPath,
getBlobPromise,
isImage: IMAGE_EXTENSIONS.includes(ext),
isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext),
};
});
/**
* Get the sort order for use with `lodash.orderBy`, and always add the
* `queryOrder` sort as the lowest priority sort order.
*/
const { sortFields } = this.state;
const fieldNames = map(sortFields, 'fieldName').concat('queryOrder');
const directions = map(sortFields, 'direction').concat('asc');
return orderBy(tableData, fieldNames, directions);
};
handleClose = () => {
this.props.closeMediaLibrary();
};
/**
* Toggle asset selection on click.
*/
handleAssetClick = asset => {
const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset;
this.setState({ selectedFile });
};
/**
* Upload a file.
*/
handlePersist = async event => {
/**
* Stop the browser from automatically handling the file input click, and
* get the file for upload, and retain the synthetic event for access after
* the asynchronous persist operation.
*/
event.stopPropagation();
event.preventDefault();
event.persist();
const { persistMedia, privateUpload } = this.props;
const { files: fileList } = event.dataTransfer || event.target;
const files = [...fileList];
const file = files[0];
await persistMedia(file, { privateUpload });
event.target.value = null;
this.scrollToTop();
};
/**
* Stores the public path of the file in the application store, where the
* editor field that launched the media library can retrieve it.
*/
handleInsert = () => {
const { selectedFile } = this.state;
const { name, url, urlIsPublicPath } = selectedFile;
const { insertMedia, publicFolder } = this.props;
const publicPath = urlIsPublicPath ? url : resolvePath(name, publicFolder);
insertMedia(publicPath);
this.handleClose();
};
/**
* Removes the selected file from the backend.
*/
handleDelete = () => {
const { selectedFile } = this.state;
const { files, deleteMedia, privateUpload } = this.props;
if (!window.confirm('Are you sure you want to delete selected media?')) {
return;
}
const file = files.find(file => selectedFile.key === file.key);
deleteMedia(file, { privateUpload }).then(() => {
this.setState({ selectedFile: {} });
});
};
handleLoadMore = () => {
const { loadMedia, dynamicSearchQuery, page, privateUpload } = this.props;
loadMedia({ query: dynamicSearchQuery, page: page + 1, privateUpload });
};
/**
* Executes media library search for implementations that support dynamic
* search via request. For these implementations, the Enter key must be
* pressed to execute search. If assets are being stored directly through
* the GitHub backend, search is in-memory and occurs as the query is typed,
* so this handler has no impact.
*/
handleSearchKeyDown = async event => {
const { dynamicSearch, loadMedia, privateUpload } = this.props;
if (event.key === 'Enter' && dynamicSearch) {
await loadMedia({ query: this.state.query, privateUpload });
this.scrollToTop();
}
};
scrollToTop = () => {
this.scrollContainerRef.scrollTop = 0;
};
/**
* Updates query state as the user types in the search field.
*/
handleSearchChange = event => {
this.setState({ query: event.target.value });
};
/**
* Filters files that do not match the query. Not used for dynamic search.
*/
queryFilter = (query, files) => {
/**
* Because file names don't have spaces, typing a space eliminates all
* potential matches, so we strip them all out internally before running the
* query.
*/
const strippedQuery = query.replace(/ /g, '');
const matches = fuzzy.filter(strippedQuery, files, { extract: file => file.name });
const matchFiles = matches.map((match, queryIndex) => {
const file = files[match.index];
return { ...file, queryIndex };
});
return matchFiles;
};
render() {
const {
isVisible,
canInsert,
2018-07-06 18:56:28 -04:00
files = [],
dynamicSearch,
dynamicSearchActive,
forImage,
isLoading,
isPersisting,
isDeleting,
hasNextPage,
isPaginating,
privateUpload,
} = this.props;
return (
2018-07-06 18:56:28 -04:00
<MediaLibraryModal
isVisible={isVisible}
canInsert={canInsert}
files={files}
dynamicSearch={dynamicSearch}
dynamicSearchActive={dynamicSearchActive}
forImage={forImage}
isLoading={isLoading}
isPersisting={isPersisting}
isDeleting={isDeleting}
hasNextPage={hasNextPage}
isPaginating={isPaginating}
privateUpload={privateUpload}
query={this.state.query}
selectedFile={this.state.selectedFile}
handleFilter={this.filterImages}
handleQuery={this.queryFilter}
toTableData={this.toTableData}
handleClose={this.handleClose}
handleSearchChange={this.handleSearchChange}
handleSearchKeyDown={this.handleSearchKeyDown}
handlePersist={this.handlePersist}
handleDelete={this.handleDelete}
handleInsert={this.handleInsert}
setScrollContainerRef={ref => (this.scrollContainerRef = ref)}
2018-07-06 18:56:28 -04:00
handleAssetClick={this.handleAssetClick}
handleLoadMore={this.handleLoadMore}
getDisplayURL={this.getDisplayURL}
2018-07-06 18:56:28 -04:00
/>
);
}
}
const mapStateToProps = state => {
const { config, mediaLibrary } = state;
const configProps = {
publicFolder: config.get('public_folder'),
};
const mediaLibraryProps = {
isVisible: mediaLibrary.get('isVisible'),
canInsert: mediaLibrary.get('canInsert'),
files: mediaLibrary.get('files'),
displayURLs: mediaLibrary.get('displayURLs'),
dynamicSearch: mediaLibrary.get('dynamicSearch'),
dynamicSearchActive: mediaLibrary.get('dynamicSearchActive'),
dynamicSearchQuery: mediaLibrary.get('dynamicSearchQuery'),
forImage: mediaLibrary.get('forImage'),
isLoading: mediaLibrary.get('isLoading'),
isPersisting: mediaLibrary.get('isPersisting'),
isDeleting: mediaLibrary.get('isDeleting'),
privateUpload: mediaLibrary.get('privateUpload'),
page: mediaLibrary.get('page'),
hasNextPage: mediaLibrary.get('hasNextPage'),
isPaginating: mediaLibrary.get('isPaginating'),
};
return { ...configProps, ...mediaLibraryProps };
};
const mapDispatchToProps = {
loadMedia: loadMediaAction,
persistMedia: persistMediaAction,
deleteMedia: deleteMediaAction,
insertMedia: insertMediaAction,
loadMediaDisplayURL: loadMediaDisplayURLAction,
closeMediaLibrary: closeMediaLibraryAction,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MediaLibrary);