2017-08-14 09:00:47 -04:00
|
|
|
import React from 'react';
|
|
|
|
import { connect } from 'react-redux';
|
2018-07-06 18:56:28 -04:00
|
|
|
import { orderBy, map } from 'lodash';
|
2017-08-14 09:00:47 -04:00
|
|
|
import fuzzy from 'fuzzy';
|
2018-07-17 19:13:52 -04:00
|
|
|
import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
|
2017-08-14 09:00:47 -04:00
|
|
|
import {
|
|
|
|
loadMedia as loadMediaAction,
|
|
|
|
persistMedia as persistMediaAction,
|
|
|
|
deleteMedia as deleteMediaAction,
|
|
|
|
insertMedia as insertMediaAction,
|
|
|
|
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';
|
2017-08-14 09:00:47 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Extensions used to determine which files to show when the media library is
|
|
|
|
* accessed from an image insertion field.
|
|
|
|
*/
|
2018-01-05 05:17:52 +10:00
|
|
|
const IMAGE_EXTENSIONS_VIEWABLE = [ 'jpg', 'jpeg', 'webp', 'gif', 'png', 'bmp', 'tiff', 'svg' ];
|
|
|
|
const IMAGE_EXTENSIONS = [ ...IMAGE_EXTENSIONS_VIEWABLE ];
|
2017-08-14 09:00:47 -04:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
2018-07-31 14:59:22 -06:00
|
|
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
2017-08-14 09:00:47 -04:00
|
|
|
/**
|
|
|
|
* 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: '' });
|
|
|
|
}
|
2018-07-31 14:59:22 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
const isOpening = !prevProps.isVisible && this.props.isVisible;
|
2017-11-19 01:56:16 -05:00
|
|
|
|
2018-07-31 14:59:22 -06:00
|
|
|
if (isOpening && (prevProps.privateUpload !== this.props.privateUpload)) {
|
|
|
|
this.props.loadMedia({ privateUpload: this.props.privateUpload });
|
2017-11-19 01:56:16 -05:00
|
|
|
}
|
2017-08-14 09:00:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
});
|
2017-08-14 09:00:47 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transform file data for table display.
|
|
|
|
*/
|
|
|
|
toTableData = files => {
|
|
|
|
const tableData = files && files.map(({ key, name, size, queryOrder, url, urlIsPublicPath }) => {
|
2017-11-13 10:58:20 -05:00
|
|
|
const ext = fileExtension(name).toLowerCase();
|
2017-08-14 09:00:47 -04:00
|
|
|
return {
|
|
|
|
key,
|
|
|
|
name,
|
|
|
|
type: ext.toUpperCase(),
|
|
|
|
size,
|
|
|
|
queryOrder,
|
|
|
|
url,
|
|
|
|
urlIsPublicPath,
|
|
|
|
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
|
2017-12-14 18:44:13 +01:00
|
|
|
* get the file for upload, and retain the synthetic event for access after
|
|
|
|
* the asynchronous persist operation.
|
2017-08-14 09:00:47 -04:00
|
|
|
*/
|
|
|
|
event.stopPropagation();
|
|
|
|
event.preventDefault();
|
2017-12-14 18:44:13 +01:00
|
|
|
event.persist();
|
2017-11-19 01:56:16 -05:00
|
|
|
const { persistMedia, privateUpload } = this.props;
|
2017-08-14 09:00:47 -04:00
|
|
|
const { files: fileList } = event.dataTransfer || event.target;
|
|
|
|
const files = [...fileList];
|
|
|
|
const file = files[0];
|
|
|
|
|
2017-11-19 01:56:16 -05:00
|
|
|
await persistMedia(file, { privateUpload });
|
2017-12-14 18:44:13 +01:00
|
|
|
|
|
|
|
event.target.value = null;
|
|
|
|
|
2017-08-14 09:00:47 -04:00
|
|
|
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;
|
2017-11-19 01:56:16 -05:00
|
|
|
const { files, deleteMedia, privateUpload } = this.props;
|
2017-08-14 09:00:47 -04:00
|
|
|
if (!window.confirm('Are you sure you want to delete selected media?')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const file = files.find(file => selectedFile.key === file.key);
|
2017-11-19 01:56:16 -05:00
|
|
|
deleteMedia(file, { privateUpload })
|
2017-08-14 09:00:47 -04:00
|
|
|
.then(() => {
|
|
|
|
this.setState({ selectedFile: {} });
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
handleLoadMore = () => {
|
2017-11-19 01:56:16 -05:00
|
|
|
const { loadMedia, dynamicSearchQuery, page, privateUpload } = this.props;
|
|
|
|
loadMedia({ query: dynamicSearchQuery, page: page + 1, privateUpload });
|
2017-08-14 09:00:47 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) => {
|
2017-11-19 01:56:16 -05:00
|
|
|
const { dynamicSearch, loadMedia, privateUpload } = this.props;
|
|
|
|
if (event.key === 'Enter' && dynamicSearch) {
|
|
|
|
await loadMedia({ query: this.state.query, privateUpload })
|
2017-08-14 09:00:47 -04:00
|
|
|
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 = [],
|
2017-08-14 09:00:47 -04:00
|
|
|
dynamicSearch,
|
|
|
|
dynamicSearchActive,
|
|
|
|
forImage,
|
|
|
|
isLoading,
|
|
|
|
isPersisting,
|
|
|
|
isDeleting,
|
|
|
|
hasNextPage,
|
|
|
|
isPaginating,
|
2017-11-19 01:56:16 -05:00
|
|
|
privateUpload,
|
2017-08-14 09:00:47 -04:00
|
|
|
} = 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}
|
|
|
|
handleAssetClick={this.handleAssetClick}
|
|
|
|
handleLoadMore={this.handleLoadMore}
|
|
|
|
/>
|
2017-08-14 09:00:47 -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'),
|
|
|
|
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,
|
|
|
|
closeMediaLibrary: closeMediaLibraryAction,
|
|
|
|
};
|
|
|
|
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(MediaLibrary);
|