diff --git a/assets/src/js/Components/ArchivePicker.js b/assets/src/js/Components/ArchivePicker.js index d8959eee60..b5cf78de43 100644 --- a/assets/src/js/Components/ArchivePicker.js +++ b/assets/src/js/Components/ArchivePicker.js @@ -1,151 +1,115 @@ -const {__} = wp.i18n; -import { Component, Fragment } from '@wordpress/element'; -import { ImagePicker } from './ImagePicker'; -import { archivePickerSidebar } from './archivePicker/archivePickerSidebar'; -import { archivePickerList } from './archivePicker/archivePickerList'; +const { __ } = wp.i18n; +import classNames from 'classnames'; +import { useState, Fragment, useEffect } from '@wordpress/element'; +import { useImages } from './archivePicker/useImages'; +import { ArchivePickerList } from './archivePicker/ArchivePickerList'; +import { SingleSidebar } from './archivePicker/SingleSidebar'; +import { MultiSidebar } from './archivePicker/MultiSidebar'; -const { apiFetch } = wp; -const { addQueryArgs } = wp.url; +const isNearScrollEnd = (event) => { + const { scrollHeight, scrollTop, clientHeight } = event.target; + const tillEnd = (scrollHeight - scrollTop - clientHeight) / scrollHeight; -class ArchivePicker extends Component { + return tillEnd < 0.2; +}; - constructor( props ) { - super( props ); - this.state = { - loading: true, - error: null, - images: [], - currentPage: 0, - filters: {}, - searchText: null, - }; - this.loadNextPage = this.loadNextPage.bind( this ); - this.updateFromUploadedResponse = this.updateFromUploadedResponse.bind( this ); - this.search = this.search.bind( this ); - this.includeInWp = this.includeInWp.bind( this ); - } +const ArchivePicker = () => { + const [searchText, setSearchText] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [enteredSearch, setEnteredSearch] = useState(null); + const [selectedIds, setSelectedIds] = useState([]); - async componentDidMount() { - await this.loadNextPage(); - } + const { + loading, + error, + loadPage, + getSelectedImages, + images, + processingError, + processingImages, + includeInWp, + } = useImages(); - async fetchImages( args ) { - return apiFetch( { - path: addQueryArgs( '/planet4/v1/image-archive/fetch', args ), - } ); - } + const isSelected = image => selectedIds.includes(image.id); - async loadNextPage( newSearch = false ) { - this.setState( { loading: true } ); + const isOnlySelected = image => selectedIds.length === 1 && selectedIds.includes(image.id); - const pageIndex = newSearch ? 1 : this.state.currentPage + 1; - const searchedText = newSearch ? this.state.enteredSearch : this.state.searchText; + const toggleSingleSelection = target => setSelectedIds(isOnlySelected(target) ? [] : [target.id]); - try { - const nextImages = await this.fetchImages( { - page: pageIndex, - search_text: searchedText, - } ); - const withPageLabel = nextImages.map( image => ( { - ...image, - pagedTitle: `${ pageIndex } -- ${ image.title }` - } ) ); - this.setState( { - currentPage: pageIndex, - images: [ - ...( newSearch ? [] : this.state.images ), - ...withPageLabel - ], - searchText: searchedText, - } ); - } catch ( error ) { - this.setState( { error } ); - } finally { - this.setState( { loading: false } ); - } - } + const toggleMultiSelection = target => setSelectedIds( + selectedIds.includes(target.id) + ? selectedIds.filter(id => id !== target.id) + : [...selectedIds, target.id] + ); - async includeInWp( ids ) { - try { - this.setState( { processingImages: true } ); - const updatedImages = await apiFetch( { - method: 'POST', - path: '/planet4/v1/image-archive/transfer', - data: { - ids: ids, - use_original_language: false, - } - } ); - this.updateFromUploadedResponse( updatedImages ); - } catch ( e ) { - console.log( e ); - this.setState( { processingError: e } ); - } finally { - this.setState( { processingImages: false } ); - } - } - - updateFromUploadedResponse( updatedImages ) { - const newImages = this.state.images.map( stateImage => { - const updated = updatedImages.find( updatedImage => updatedImage.id === stateImage.id ); - if ( updated ) { - return updated; - } - return stateImage; - } ); - this.setState( { - images: newImages, - } ); - } - - async search() { - await this.loadNextPage( true ); - } + useEffect(() => { + loadPage(currentPage, searchText); + }, [currentPage, searchText]); - render() { - const { - loading, - error, - images, - } = this.state; + const selectedImages = getSelectedImages(selectedIds); - return -
{ - event.preventDefault(); - this.search(); - } } - onChange={ event => this.setState( { enteredSearch: event.target.value } ) } + return + { + event.preventDefault(); + if (!loading) { + setSearchText(enteredSearch); + setCurrentPage(1); + } + }} + onChange={event => setEnteredSearch(event.target.value)} + > + + + + {loading && ( +
loading...
+ )} + {!!error && ( +
+

API error:

+

{error.message}

+
+ )} +
+
    { + if (!loading && isNearScrollEnd(event)) { + setCurrentPage(currentPage + 1); + } + }} > - - - - { loading && ( -
    loading...
    - ) } - { !!error && ( -
    -

    API error:

    -

    { error.message }

    -
    - ) } - { - if ( !this.state.loading ) { - await this.loadNextPage(); +
+ {selectedImages.length > 0 && ( +
+ {selectedImages.length === 1 ? + : + } - } } - /> - ; - } -} +
+ )} +
+
; +}; export default ArchivePicker; diff --git a/assets/src/js/Components/ImagePicker.js b/assets/src/js/Components/ImagePicker.js deleted file mode 100644 index 30562cfc52..0000000000 --- a/assets/src/js/Components/ImagePicker.js +++ /dev/null @@ -1,94 +0,0 @@ -import { Component } from '@wordpress/element'; -import classNames from 'classnames'; - -export const getSizesUnder = ( sizes, maxWidth) => sizes.filter( size => { - if ( maxWidth ) { - return size.width < maxWidth; - } - return true; -} ); - -export const toSrcSet = ( sizes, config ) => { - const sizesToUse = config && config.maxWidth ? getSizesUnder( sizes, config.maxWidth ) : sizes; - - return sizesToUse.map( size => `${ size.url } ${ size.width }w` ).join(); -}; - -const isNearScrollEnd = ( event ) => { - const { scrollHeight, scrollTop, clientHeight } = event.target; - const tillEnd = ( scrollHeight - scrollTop - clientHeight ) / scrollHeight; - - return tillEnd < 0.2; -}; - -export class ImagePicker extends Component { - - constructor( props ) { - super( props ); - this.state = { - selectedIds: [], - }; - this.isSelected = this.isSelected.bind( this ); - this.isOnlySelected = this.isOnlySelected.bind( this ); - this.toggleSingleSelection = this.toggleSingleSelection.bind( this ); - this.toggleMultiSelection = this.toggleMultiSelection.bind( this ); - this.getSelectedImages = this.getSelectedImages.bind( this ); - } - - isSelected( image ) { - return this.state.selectedIds.includes( image.id ); - } - - isOnlySelected( image ) { - return this.state.selectedIds.length === 1 && this.state.selectedIds.includes( image.id ); - } - - toggleSingleSelection( target ) { - this.setState( { - selectedIds: this.isOnlySelected( target ) ? [] : [ target.id ], - } ); - } - - toggleMultiSelection( target ) { - this.setState( { - selectedIds: - this.state.selectedIds.includes( target.id ) - ? this.state.selectedIds.filter( id => id !== target.id ) - : [ ...this.state.selectedIds, target.id ] - } ); - } - - getSelectedImages() { - return this.state.selectedIds - .map( selected => this.props.images.find( image => image.id === selected ) ) - .filter( image => !!image ); - } - - render() { - const { - renderList = () => '', - renderSidebar = () => '', - onNearListBottom = async () => null, - } = this.props; - - const selectedImages = this.getSelectedImages(); - - return
- - { selectedImages.length > 0 && ( -
- { renderSidebar( this ) } -
- ) } -
; - } -} diff --git a/assets/src/js/Components/archivePicker/ArchivePickerList.js b/assets/src/js/Components/archivePicker/ArchivePickerList.js new file mode 100644 index 0000000000..6aa42d5bac --- /dev/null +++ b/assets/src/js/Components/archivePicker/ArchivePickerList.js @@ -0,0 +1,49 @@ +import classNames from 'classnames'; +import { toSrcSet } from './sizeFunctions'; + +export const ArchivePickerList = ({ + isSelected, + toggleSingleSelection, + toggleMultiSelection, + images +}) => { + + return !images ? '' : images.map(image => { + const { + id, + sizes, + title, + alt, + wordpress_id, + original, + } = image; + + try { + + return
  • + {alt} + (event.ctrlKey || event.metaKey) // metaKey for Mac users + ? toggleMultiSelection(image) + : toggleSingleSelection(image) + } + /> +
  • ; + } catch (exception) { + return
  • + {image.title} + No image available. +
  • ; + } + }); +}; diff --git a/assets/src/js/Components/archivePicker/MultiSidebar.js b/assets/src/js/Components/archivePicker/MultiSidebar.js index 88fd2a1cd7..9b62f39fc7 100644 --- a/assets/src/js/Components/archivePicker/MultiSidebar.js +++ b/assets/src/js/Components/archivePicker/MultiSidebar.js @@ -1,42 +1,25 @@ -import { Component, Fragment } from '@wordpress/element'; -import { toSrcSet } from '../ImagePicker'; +import { Fragment } from '@wordpress/element'; +import { toSrcSet } from './sizeFunctions'; const { __ } = wp.i18n; -export class MultiSidebar extends Component { - constructor( props ) { - super( props ); - this.state = { - processingImages: false, - processingError: null, - }; - } - - render() { - const { parent } = this.props; - const { getSelectedImages, toggleMultiSelection } = parent; - - const selectedImages = getSelectedImages(); - - return -

    { selectedImages.length }{ __( 'images selected', 'planet4-master-theme-backend' ) }

    - -
    ; - } - -} +export const MultiSidebar = ({ selectedImages, toggleMultiSelection }) => ( + +

    {selectedImages.length} {__('images selected', 'planet4-master-theme-backend')}

    + +
    +); diff --git a/assets/src/js/Components/archivePicker/SingleSidebar.js b/assets/src/js/Components/archivePicker/SingleSidebar.js index 6f0fdbaafb..1dc3219c73 100644 --- a/assets/src/js/Components/archivePicker/SingleSidebar.js +++ b/assets/src/js/Components/archivePicker/SingleSidebar.js @@ -1,135 +1,93 @@ -import { Component, Fragment } from '@wordpress/element'; -import { getSizesUnder, toSrcSet } from '../ImagePicker'; +import { Fragment } from '@wordpress/element'; +import { toSrcSet } from './sizeFunctions'; const { __ } = wp.i18n; const PREVIEW_MAX_SIZE = 1300; -const wpImageLink = ( id ) => `${ window.location.href.split( '/wp-admin' )[ 0 ] }/wp-admin/post.php?post=${ id }&action=edit`; +const wpImageLink = id => `${window.location.href.split('/wp-admin')[0]}/wp-admin/post.php?post=${id}&action=edit`; -const largestSize = ( image ) => !image ? null : image.original; +const renderDefinition = (key, value) => ( +
    +
    {key}
    +
    {value}
    +
    +); -const renderDefinition = ( key, value ) => (
    -
    { key }
    -
    { value }
    -
    ); +export const SingleSidebar = ({ image, processingError, processingImages, includeInWp }) => { + const original = image ? image.original : {}; + const aspectRatio = original.height / original.width; -export class SingleSidebar extends Component { - constructor( props ) { - super( props ); - this.state = { - imageLoaded: false, - }; - this.renderImage = this.renderImage.bind(this); - this.preloadImage = this.preloadImage.bind(this); - } - - componentDidMount() { - this.preloadImage(); - } - - async componentDidUpdate( prevProps ) { - if ( prevProps.image !== this.props.image ) { - this.preloadImage(); - } - } - - async preloadImage() { - this.setState( { imageLoaded: false } ); - await fetch( largestSize( getSizesUnder( this.props.image.sizes, PREVIEW_MAX_SIZE ) ) ); - this.setState( { imageLoaded: true } ); - } - - renderImage () { - - const { image } = this.props; - const { original } = image; - const aspectRatio = original.height / original.width ; - - return
    ( +
    - {! this.state.imageLoaded - ? __( 'Loading image', 'planet4-master-theme-backend' ) - : ( - { - ) } - + {image.title}
    -
    ; - } - - render() { - const { - image, - includeInWp = async () => null, - processingError, - processingImages, - } = this.props; - - const original = largestSize( image ); - - return - { !!processingError && ( -
    Error: { processingError.message }
    - ) } - { !!processingImages && ( -
    Processing...
    - ) } - { image.wordpress_id ? ( +
    + ); + + return ( + + {!!processingError && ( +
    Error: { processingError.message}
    + )} + {!!processingImages && ( +
    Processing...
    + )} + {image.wordpress_id ? ( Wordpress image #{ image.wordpress_id } + href={wpImageLink(image.wordpress_id)} + >Wordpress image #{ image.wordpress_id} ) : ( - ) } - { this.renderImage() } -
    - { renderDefinition( - __( 'URL', 'planet4-master-theme-backend' ), - { original.url } - ) } - { renderDefinition( - __( 'Dimensions', 'planet4-master-theme-backend' ), - `${ original.width } x ${ original.height }` - ) } - { renderDefinition( - __( 'Title', 'planet4-master-theme-backend' ), + )} + {renderImage()} +
    + {renderDefinition( + __('URL', 'planet4-master-theme-backend'), + {original.url} + )} + {renderDefinition( + __('Dimensions', 'planet4-master-theme-backend'), + `${original.width} x ${original.height}` + )} + {renderDefinition( + __('Title', 'planet4-master-theme-backend'), image.title - ) } - { renderDefinition( - __( 'Caption', 'planet4-master-theme-backend' ), + )} + {renderDefinition( + __('Caption', 'planet4-master-theme-backend'), image.caption - ) } - { renderDefinition( - __( 'Credit', 'planet4-master-theme-backend' ), + )} + {renderDefinition( + __('Credit', 'planet4-master-theme-backend'), image.credit - ) } - { renderDefinition( - __( 'Original language title', 'planet4-master-theme-backend' ), + )} + {renderDefinition( + __('Original language title', 'planet4-master-theme-backend'), image.original_language_title, - ) } - { renderDefinition( - __( 'Original language description', 'planet4-master-theme-backend' ), + )} + {renderDefinition( + __('Original language description', 'planet4-master-theme-backend'), image.original_language_description, - ) } + )}
    - ; - } - -} + + ); +}; diff --git a/assets/src/js/Components/archivePicker/archivePickerList.js b/assets/src/js/Components/archivePicker/archivePickerList.js deleted file mode 100644 index 4b897e033b..0000000000 --- a/assets/src/js/Components/archivePicker/archivePickerList.js +++ /dev/null @@ -1,48 +0,0 @@ -import classNames from 'classnames'; -import { toSrcSet } from '../ImagePicker'; - -export const archivePickerList = () => ( imagePicker ) => { - - const { props, isSelected, toggleMultiSelection, toggleSingleSelection } = imagePicker; - - const { images } = props; - - return !images ? '' : images.map( image => { - const { - id, - sizes, - title, - alt, - wordpress_id, - original, - } = image; - - try { - - return
  • - { - (event.ctrlKey || event.metaKey) // metaKey for Mac users - ? toggleMultiSelection( image ) - : toggleSingleSelection( image ) - } - /> -
  • ; - } catch ( exception ) { - return
  • - { image.title } - No image available. -
  • ; - } - } ); -}; diff --git a/assets/src/js/Components/archivePicker/archivePickerSidebar.js b/assets/src/js/Components/archivePicker/archivePickerSidebar.js deleted file mode 100644 index ac460cba8a..0000000000 --- a/assets/src/js/Components/archivePicker/archivePickerSidebar.js +++ /dev/null @@ -1,22 +0,0 @@ -import { SingleSidebar } from './SingleSidebar'; -import { MultiSidebar } from './MultiSidebar'; - -export const archivePickerSidebar = ( archivePicker ) => ( imagePicker ) => { - const selectedImages = imagePicker.getSelectedImages(); - - if ( selectedImages.length === 1 ) { - return ; - } - - if ( selectedImages.length > 1 ) { - return ; - } -}; diff --git a/assets/src/js/Components/archivePicker/sizeFunctions.js b/assets/src/js/Components/archivePicker/sizeFunctions.js new file mode 100644 index 0000000000..c10c05684a --- /dev/null +++ b/assets/src/js/Components/archivePicker/sizeFunctions.js @@ -0,0 +1,12 @@ +export const getSizesUnder = ( sizes, maxWidth) => sizes.filter( size => { + if ( maxWidth ) { + return size.width < maxWidth; + } + return true; +} ); + +export const toSrcSet = ( sizes, config ) => { + const sizesToUse = config && config.maxWidth ? getSizesUnder( sizes, config.maxWidth ) : sizes; + + return sizesToUse.map( size => `${ size.url } ${ size.width }w` ).join(); +}; diff --git a/assets/src/js/Components/archivePicker/useImages.js b/assets/src/js/Components/archivePicker/useImages.js new file mode 100644 index 0000000000..1e55688a78 --- /dev/null +++ b/assets/src/js/Components/archivePicker/useImages.js @@ -0,0 +1,81 @@ +import { useState } from '@wordpress/element'; + +const { apiFetch } = wp; +const { addQueryArgs } = wp.url; + +export const useImages = () => { + const [images, setImages] = useState([]); + const [loading, setLoading] = useState(false); + const [processingImages, setProcessingImages] = useState(false); + const [processingError, setProcessingError] = useState(null); + const [error, setError] = useState(null); + + const loadPage = async (pageIndex, searchedText) => { + if (loading) { + return; + } + setLoading(true); + + try { + const nextImages = await apiFetch({ + path: addQueryArgs('/planet4/v1/image-archive/fetch', { + page: pageIndex, + search_text: searchedText, + }) + }); + setImages([ + ...(pageIndex === 1 ? [] : images), + ...nextImages + ]); + } catch (error) { + setError(error); + } finally { + setLoading(false); + } + }; + + const includeInWp = async ids => { + try { + setProcessingImages(true); + const updatedImages = await apiFetch({ + method: 'POST', + path: '/planet4/v1/image-archive/transfer', + data: { + ids: ids, + use_original_language: false, + } + }); + updateFromUploadedResponse(updatedImages); + } catch (e) { + setProcessingError(e); + } finally { + setProcessingImages(false); + } + }; + + const updateFromUploadedResponse = updatedImages => { + const newImages = images.map(stateImage => { + const updated = updatedImages.find(updatedImage => updatedImage.id === stateImage.id); + if (updated) { + return updated; + } + return stateImage; + }); + setImages(newImages); + }; + + const getSelectedImages = selectedIds => selectedIds + .map(selected => images.find(image => image.id === selected)) + .filter(image => !!image); + + return { + images, + loading, + error, + loadPage, + includeInWp, + processingError, + processingImages, + getSelectedImages, + }; +};