Making WordPress.org

Changeset 8711


Ignore:
Timestamp:
05/01/2019 10:10:19 PM (6 years ago)
Author:
coreymckrill
Message:

WordCamp Blocks: Refactor Sessions to use data store, other improvements

  • Memoize the select options
  • Add missing doc blocks
  • JS linting cleanup

Props vedjain, coreymckrill

Location:
sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/block-content.js

    r8668 r8711  
    22 * External dependencies
    33 */
    4 import { get } from 'lodash';
     4import { get }    from 'lodash';
    55import classnames from 'classnames';
    66
     
    99 */
    1010const { Component } = wp.element;
    11 const { __ } = wp.i18n;
     11const { __ }        = wp.i18n;
    1212
    1313/**
    1414 * Internal dependencies
    1515 */
    16 import {ItemTitle, ItemHTMLContent, ItemPermalink} from '../shared/block-content';
    17 import { tokenSplit, arrayTokenReplace, intersperse, listify } from '../shared/i18n';
    18 import GridContentLayout from '../shared/grid-layout/block-content';
    19 import FeaturedImage from '../shared/featured-image';
    20 
     16import { ItemTitle, ItemHTMLContent, ItemPermalink, BlockNoContent } from '../shared/block-content';
     17import { tokenSplit, arrayTokenReplace, intersperse, listify }       from '../shared/i18n';
     18import GridContentLayout                                             from '../shared/grid-layout/block-content';
     19import FeaturedImage                                                 from '../shared/featured-image';
     20import { filterEntities }                                            from '../blocks-store';
     21
     22/**
     23 * Component for the section of each session post that displays information about the session's speakers.
     24 *
     25 * @param {Object} session
     26 *
     27 * @return {Element}
     28 */
    2129function SessionSpeakers( { session } ) {
    2230    let speakerData = get( session, '_embedded.speakers', [] );
    2331
    2432    speakerData = speakerData.map( ( speaker ) => {
    25         const { link = '' } = speaker;
    26         let {  title = {} } = speaker;
     33        if ( speaker.hasOwnProperty( 'code' ) ) {
     34            // The wporg username given for this speaker returned an error.
     35            return null;
     36        }
     37
     38        const { link }     = speaker;
     39        let { title = {} } = speaker;
    2740
    2841        title = title.rendered.trim() || __( 'Unnamed', 'wordcamporg' );
     
    3346
    3447        return (
    35             <a
    36                 key={ link }
    37                 href={ link }
    38             >
     48            <a key={ link } href={ link }>
    3949                { title }
    4050            </a>
     
    5565}
    5666
     67/**
     68 * Component for the section of each session post that displays metadata including date, time, and location (track).
     69 *
     70 * @param {Object} session
     71 *
     72 * @return {Element}
     73 */
    5774function SessionMeta( { session } ) {
    5875    let metaContent;
     
    98115}
    99116
     117/**
     118 * Component for the section of each session post that displays a session's assigned categories.
     119 *
     120 * @param {Object} session
     121 *
     122 * @return {Element}
     123 */
    100124function SessionCategory( { session } ) {
    101125    let categoryContent;
     
    130154}
    131155
     156/**
     157 * Component for displaying the block content.
     158 */
    132159class SessionsBlockContent extends Component {
    133     hasSpeaker( session ) {
     160    /**
     161     * Run additional operations during component initialization.
     162     *
     163     * @param {Object} props
     164     */
     165    constructor( props ) {
     166        super( props );
     167
     168        this.getFilteredPosts = this.getFilteredPosts.bind( this );
     169    }
     170
     171    /**
     172     * Determine if a session has related speaker data.
     173     *
     174     * @param {Object} session
     175     *
     176     * @return {boolean}
     177     */
     178    static hasSpeaker( session ) {
    134179        return get( session, '_embedded.speakers', [] ).length > 0;
    135180    }
    136181
     182    /**
     183     * Filter and sort the content that will be rendered.
     184     *
     185     * @returns {Array}
     186     */
     187    getFilteredPosts() {
     188        const { attributes, entities } = this.props;
     189        const { wcb_session: posts } = entities;
     190        const { mode, item_ids, sort } = attributes;
     191
     192        const args = {};
     193
     194        if ( Array.isArray( item_ids ) && item_ids.length > 0 ) {
     195            let fieldName;
     196
     197            switch ( mode ) {
     198                case 'wcb_session':
     199                    fieldName = 'id';
     200                    break;
     201                case 'wcb_track':
     202                    fieldName = 'session_track';
     203                    break;
     204                case 'wcb_session_category':
     205                    fieldName = 'session_category';
     206                    break;
     207            }
     208
     209            args.filter  = [
     210                {
     211                    fieldName  : fieldName,
     212                    fieldValue : item_ids,
     213                },
     214            ];
     215        }
     216
     217        if ( 'session_time' !== sort ) {
     218            args.sort = sort;
     219        }
     220
     221        let filtered = filterEntities( posts, args );
     222
     223        if ( Array.isArray( filtered ) && 'session_time' === sort ) {
     224            filtered = filtered.sort( ( a, b ) => {
     225                return Number( a.meta._wcpt_session_time ) - Number( b.meta._wcpt_session_time );
     226            } );
     227        }
     228
     229        return filtered;
     230    }
     231
     232    /**
     233     * Render the block content.
     234     *
     235     * @return {Element}
     236     */
    137237    render() {
    138         const { attributes, sessionPosts } = this.props;
     238        const { attributes } = this.props;
    139239        const { show_speaker, show_images, image_align, featured_image_width, content, show_meta, show_category } = attributes;
     240
     241        const posts     = this.getFilteredPosts();
     242        const isLoading = ! Array.isArray( posts );
     243        const hasPosts  = ! isLoading && posts.length > 0;
     244
     245        if ( isLoading || ! hasPosts ) {
     246            return (
     247                <BlockNoContent loading={ isLoading } />
     248            );
     249        }
    140250
    141251        return (
     
    144254                { ...this.props }
    145255            >
    146                 { sessionPosts.map( ( post ) =>
     256                { posts.map( ( post ) =>
    147257                    <div
    148258                        key={ post.slug }
     
    161271                        />
    162272
    163                         { show_speaker && this.hasSpeaker( post ) &&
     273                        { show_speaker && this.constructor.hasSpeaker( post ) &&
    164274                            <SessionSpeakers session={ post } />
    165275                        }
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/block-controls.js

    r8526 r8711  
    88 */
    99const { Button, Placeholder } = wp.components;
    10 const { __ } = wp.i18n;
     10const { __ }                  = wp.i18n;
    1111
    1212/**
    1313 * Internal dependencies
    1414 */
    15 import { BlockControls, PlaceholderNoContent, PlaceholderSpecificMode } from '../shared/block-controls';
    16 import SessionsBlockContent from './block-content';
    17 import SessionsSelect from './sessions-select';
    18 import { LABEL }                                                        from './index';
     15import { BlockControls, PlaceholderSpecificMode } from '../shared/block-controls';
     16import SessionsBlockContent                       from './block-content';
     17import SessionsSelect                             from './sessions-select';
     18import { LABEL }                                  from './index';
    1919
     20/**
     21 * Component for displaying a UI within the block.
     22 */
    2023class SessionsBlockControls extends BlockControls {
     24    /**
     25     * Render the internal block UI.
     26     *
     27     * @return {Element}
     28     */
    2129    render() {
    22         const { icon, attributes, setAttributes, sessionPosts } = this.props;
     30        const { icon, attributes, setAttributes } = this.props;
    2331        const { mode } = attributes;
    24 
    25         const hasPosts = Array.isArray( sessionPosts ) && sessionPosts.length;
    26 
    27         if ( mode && ! hasPosts ) {
    28             return (
    29                 <PlaceholderNoContent
    30                     icon={ icon }
    31                     label={ LABEL }
    32                     loading={ ! Array.isArray( sessionPosts ) }
    33                 />
    34             );
    35         }
    3632
    3733        let output;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/edit.js

    r8601 r8711  
    1 /**
    2  * External dependencies
    3  */
    4 import { isUndefined, pickBy, split } from 'lodash';
    5 
    61/**
    72 * WordPress dependencies
    83 */
    9 const apiFetch = wp.apiFetch;
    10 const { withSelect } = wp.data;
     4const { withSelect }          = wp.data;
    115const { Component, Fragment } = wp.element;
    12 const { addQueryArgs } = wp.url;
    136
    147/**
    158 * Internal dependencies
    169 */
    17 import SessionsBlockControls from './block-controls';
     10import SessionsBlockControls     from './block-controls';
    1811import SessionsInspectorControls from './inspector-controls';
    19 import GridToolbar from '../shared/grid-layout/toolbar';
     12import GridToolbar               from '../shared/grid-layout/toolbar';
    2013import { ICON }                  from './index';
     14import { WC_BLOCKS_STORE }       from '../blocks-store';
    2115
    2216const blockData = window.WordCampBlocks.sessions || {};
    23 const MAX_POSTS = 100;
    2417
    25 const ALL_POSTS_QUERY = {
    26     orderby  : 'title',
    27     order    : 'asc',
    28     per_page : MAX_POSTS,
    29     _embed   : true,
    30 };
    31 
    32 const ALL_TERMS_QUERY = {
    33     orderby  : 'name',
    34     order    : 'asc',
    35     per_page : MAX_POSTS,
    36 };
    37 
     18/**
     19 * Top-level component for the editing UI for the block.
     20 */
    3821class SessionsEdit extends Component {
    39     constructor( props ) {
    40         super( props );
    41 
    42         this.fetchSessionDetails();
    43     }
    44 
    45     fetchSessionDetails() {
    46         const allSessionPosts = apiFetch( {
    47             path: addQueryArgs( `/wp/v2/sessions`, ALL_POSTS_QUERY ),
    48         } );
    49         const allSessionTracks = apiFetch( {
    50             path: addQueryArgs( `/wp/v2/session_track`, ALL_TERMS_QUERY ),
    51         } );
    52         const allSessionCategories = apiFetch( {
    53             path: addQueryArgs( `/wp/v2/session_category`, ALL_TERMS_QUERY ),
    54         } );
    55 
    56         this.state = {
    57             allSessionPosts: allSessionPosts, // Promise
    58             allSessionTracks: allSessionTracks, // Promise
    59             allSessionCategories: allSessionCategories, // Promise
    60         }
    61     }
    62 
     22    /**
     23     * Render the block's editing UI.
     24     *
     25     * @return {Element}
     26     */
    6327    render() {
    6428        const { mode } = this.props.attributes;
     
    6933                    icon={ ICON }
    7034                    { ...this.props }
    71                     { ...this.state }
    7235                />
    7336                { mode &&
    74                 <Fragment>
    75                     <SessionsInspectorControls { ...this.props } />
    76                     <GridToolbar { ...this.props } />
    77                 </Fragment>
     37                    <Fragment>
     38                        <SessionsInspectorControls { ...this.props } />
     39                        <GridToolbar { ...this.props } />
     40                    </Fragment>
    7841                }
    7942            </Fragment>
     
    8245}
    8346
    84 const sessionsSelect = ( select, props ) => {
    85     const { mode, item_ids, sort } = props.attributes;
    86     const { getEntityRecords } = select( 'core' );
     47const sessionsSelect = ( select ) => {
     48    const { getEntities } = select( WC_BLOCKS_STORE );
    8749
    88     const args = {
    89         per_page           : MAX_POSTS, // -1 is not allowed for per_page.
    90         _embed             : true,
    91         context            : 'view',
    92         _wcpt_session_type : 'session',
     50    const entities = {
     51        wcb_session          : getEntities( 'postType', 'wcb_session', { _embed: true } ),
     52        wcb_track            : getEntities( 'taxonomy', 'wcb_track' ),
     53        wcb_session_category : getEntities( 'taxonomy', 'wcb_session_category' ),
    9354    };
    9455
    95     if ( 'session_time' !== sort ) {
    96         const [ orderby, order ] = split( sort, '_', 2 );
    97         args.orderby = orderby;
    98         args.order = order;
    99     }
    100 
    101     if ( Array.isArray( item_ids ) ) {
    102         switch ( mode ) {
    103             case 'wcb_session':
    104                 args.include = item_ids;
    105                 break;
    106             case 'wcb_track':
    107                 args.session_track = item_ids;
    108                 break;
    109             case 'wcb_session_category':
    110                 args.session_category = item_ids;
    111                 break;
    112         }
    113     }
    114 
    115     const sessionsQuery = pickBy( args, ( value ) => ! isUndefined( value ) );
    116 
    117     const sessionPosts = getEntityRecords( 'postType', 'wcb_session', sessionsQuery );
    118 
    119     // todo Is there a way to do this sorting via REST API parameters?
    120     if ( Array.isArray( sessionPosts ) && 'session_time' === sort ) {
    121         sessionPosts.sort( ( a, b ) => {
    122             return Number( a.meta._wcpt_session_time ) - Number( b.meta._wcpt_session_time );
    123         } );
    124     }
    125 
    126     return { blockData, sessionPosts };
     56    return {
     57        blockData,
     58        entities,
     59    };
    12760};
    12861
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/index.js

    r8627 r8711  
    1414
    1515const supports = {
    16     'align': [ 'wide', 'full' ],
     16    align: [ 'wide', 'full' ],
    1717};
    1818
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/inspector-controls.js

    r8647 r8711  
    1010 * Internal dependencies
    1111 */
    12 import GridInspectorControl from '../shared/grid-layout/inspector-control';
     12import GridInspectorControl           from '../shared/grid-layout/inspector-control';
    1313import FeaturedImageInspectorControls from '../shared/featured-image/inspector-control';
    1414
     15/**
     16 * Component for block controls that appear in the Inspector Panel.
     17 */
    1518class SessionsInspectorControls extends Component {
     19    /**
     20     * Render the controls.
     21     *
     22     * @return {Element}
     23     */
    1624    render() {
    1725        const { attributes, setAttributes, blockData } = this.props;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/sessions/sessions-select.js

    r8633 r8711  
    22 * External dependencies
    33 */
    4 import { get, includes } from 'lodash';
     4import { every, flatMap, includes } from 'lodash';
    55
    66/**
    77 * WordPress dependencies
    88 */
    9 const { Dashicon } = wp.components;
    109const { Component } = wp.element;
    11 const { __ } = wp.i18n;
     10const { __ }        = wp.i18n;
    1211
    1312/**
    1413 * Internal dependencies
    1514 */
    16 import ItemSelect from '../shared/item-select';
     15import ItemSelect, { buildOptions, Option } from '../shared/item-select';
    1716
     17/**
     18 * Component for selecting posts/terms for populating the block content.
     19 */
    1820class SessionsSelect extends Component {
     21    /**
     22     * Run additional operations during component initialization.
     23     *
     24     * @param {Object} props
     25     */
    1926    constructor( props ) {
    2027        super( props );
    2128
    22         this.state = {
    23             wcb_session          : [],
    24             wcb_track            : [],
    25             wcb_session_category : [],
    26             loading              : true,
    27         };
    28 
    29         this.buildSelectOptions = this.buildSelectOptions.bind( this );
    30         this.fetchSelectOptions( props );
     29        this.buildSelectOptions    = this.buildSelectOptions.bind( this );
     30        this.getCurrentSelectValue = this.getCurrentSelectValue.bind( this );
     31        this.isLoading             = this.isLoading.bind( this );
    3132    }
    3233
    33     fetchSelectOptions( props ) {
    34         const { allSessionPosts, allSessionTracks, allSessionCategories } = props;
    35         const promises = [];
     34    /**
     35     * Build or retrieve the options that will populate the Select dropdown.
     36     *
     37     * @return {Array}
     38     */
     39    buildSelectOptions() {
     40        const { entities } = this.props;
     41        const { wcb_session, wcb_track, wcb_session_category } = entities;
    3642
    37         promises.push( allSessionPosts.then(
    38             ( fetchedPosts ) => {
    39                 const posts = fetchedPosts.map( ( post ) => {
    40                     const image = get( post, '_embedded[\'wp:featuredmedia\'].media_details.sizes.thumbnail.source_url', '' );
     43        const optionGroups = [
     44            {
     45                entityType : 'post',
     46                type       : 'wcb_session',
     47                label      : __( 'Sessions', 'wordcamporg' ),
     48                items      : wcb_session,
     49            },
     50            {
     51                entityType : 'term',
     52                type       : 'wcb_track',
     53                label      : __( 'Tracks', 'wordcamporg' ),
     54                items      : wcb_track,
     55            },
     56            {
     57                entityType : 'term',
     58                type       : 'wcb_session_category',
     59                label      : __( 'Session Categories', 'wordcamporg' ),
     60                items      : wcb_session_category,
     61            },
     62        ];
    4163
    42                     return {
    43                         label : post.title.rendered.trim() || __( '(Untitled)', 'wordcamporg' ),
    44                         value : post.id,
    45                         type  : 'wcb_session',
    46                         image : image,
    47                     };
    48                 } );
    49 
    50                 this.setState( { wcb_session: posts } );
    51             }
    52         ).catch() );
    53 
    54         [ allSessionTracks, allSessionCategories ].forEach( ( promise ) => {
    55             promises.push( promise.then(
    56                 ( fetchedTerms ) => {
    57                     const terms = fetchedTerms.map( ( term ) => {
    58                         return {
    59                             label : term.name.trim() || __( '(Untitled)', 'wordcamporg' ),
    60                             value : term.id,
    61                             type  : term.taxonomy,
    62                             count : term.count || 0,
    63                         };
    64                     } );
    65 
    66                     const [ firstTerm ] = terms;
    67                     this.setState( { [ firstTerm.type ]: terms } );
    68                 }
    69             ).catch() );
    70         } );
    71 
    72         Promise.all( promises ).then( () => {
    73             this.setState( { loading: false } );
    74         } );
     64        return buildOptions( optionGroups );
    7565    }
    7666
    77     buildSelectOptions( mode ) {
    78         const { getOwnPropertyDescriptors } = Object;
    79         const options = [];
     67    /**
     68     * Determine the currently selected options in the Select dropdown based on block attributes.
     69     *
     70     * @return {Array}
     71     */
     72    getCurrentSelectValue() {
     73        const { attributes } = this.props;
     74        const { mode, item_ids } = attributes;
    8075
    81         const labels = {
    82             wcb_session          : __( 'Sessions', 'wordcamporg' ),
    83             wcb_track            : __( 'Tracks', 'wordcamporg' ),
    84             wcb_session_category : __( 'Session Categories', 'wordcamporg' ),
    85         };
    86 
    87         for ( const type in getOwnPropertyDescriptors( this.state ) ) {
    88             if ( ( ! mode || type === mode ) && this.state[ type ].length ) {
    89                 options.push( {
    90                     label   : labels[ type ],
    91                     options : this.state[ type ],
    92                 } );
    93             }
    94         }
    95 
    96         return options;
    97     }
    98 
    99     render() {
    100         const { icon, label, attributes, setAttributes } = this.props;
    101         const { mode, item_ids } = attributes;
    102         const options = this.buildSelectOptions( mode );
     76        const options = flatMap( this.buildSelectOptions(), ( group ) => {
     77            return group.options;
     78        } );
    10379
    10480        let value = [];
    10581
    10682        if ( mode && item_ids.length ) {
    107             const modeOptions = get( options, '[0].options', [] );
    108 
    109             value = modeOptions.filter( ( option ) => {
    110                 return includes( item_ids, option.value );
     83            value = options.filter( ( option ) => {
     84                return mode === option.type && includes( item_ids, option.value );
    11185            } );
    11286        }
     87
     88        return value;
     89    }
     90
     91    /**
     92     * Check if all of the entity groups have finished loading.
     93     *
     94     * @return {boolean}
     95     */
     96    isLoading() {
     97        const { entities } = this.props;
     98
     99        return ! every( entities, ( value ) => {
     100            return Array.isArray( value );
     101        } );
     102    }
     103
     104    /**
     105     * Render an ItemSelect component with block-specific settings.
     106     *
     107     * @return {Element}
     108     */
     109    render() {
     110        const { icon, label, setAttributes } = this.props;
    113111
    114112        return (
     
    116114                className="wordcamp-sessions-select"
    117115                label={ label }
    118                 value={ value }
    119                 buildSelectOptions={ this.buildSelectOptions }
     116                value={ this.getCurrentSelectValue() }
    120117                onChange={ ( changed ) => setAttributes( changed ) }
    121                 mode={ mode }
    122118                selectProps={ {
    123                     isLoading        : this.state.loading,
    124                     formatGroupLabel : ( groupData ) => {
     119                    options           : this.buildSelectOptions(),
     120                    isLoading         : this.isLoading(),
     121                    formatOptionLabel : ( optionData ) => {
    125122                        return (
    126                             <span className="wordcamp-item-select-option-group-label">
    127                                 { groupData.label }
    128                             </span>
    129                         );
    130                     },
    131                     formatOptionLabel: ( optionData ) => {
    132                         return (
    133                             <SessionsOption
    134                                 icon={ icon }
     123                            <Option
     124                                icon={ includes( [ 'wcb_track', 'wcb_session_category' ], optionData.type ) ? icon : null }
    135125                                { ...optionData }
    136126                            />
     
    143133}
    144134
    145 function SessionsOption( { type, icon, label = '', image = '', count = 0 } ) {
    146     let optImage, optContent;
    147 
    148     switch ( type ) {
    149         case 'wcb_session' :
    150             if ( image ) {
    151                 optImage = (
    152                     <img
    153                         className="wordcamp-item-select-option-image"
    154                         src={ image }
    155                         alt={ label }
    156                         width={ 24 }
    157                         height={ 24 }
    158                     />
    159                 );
    160             } else {
    161                 optImage = (
    162                     <div className="wordcamp-item-select-option-icon-container">
    163                         <Dashicon
    164                             className="wordcamp-item-select-option-icon"
    165                             icon={ icon }
    166                             size={ 16 }
    167                         />
    168                     </div>
    169                 );
    170             }
    171             optContent = (
    172                 <span className="wordcamp-item-select-option-label">
    173                     { label }
    174                 </span>
    175             );
    176             break;
    177 
    178         case 'wcb_track' :
    179         case 'wcb_session_category' :
    180             optImage = (
    181                 <div className="wordcamp-item-select-option-icon-container">
    182                     <Dashicon
    183                         className="wordcamp-item-select-option-icon"
    184                         icon={ icon }
    185                         size={ 16 }
    186                     />
    187                 </div>
    188             );
    189             optContent = (
    190                 <span className="wordcamp-item-select-option-label">
    191                     { label }
    192                     <span className="wordcamp-item-select-option-label-term-count">
    193                         { count }
    194                     </span>
    195                 </span>
    196             );
    197             break;
    198     }
    199 
    200     return (
    201         <div className="wordcamp-item-select-option">
    202             { optImage }
    203             { optContent }
    204         </div>
    205     );
    206 }
    207 
    208135export default SessionsSelect;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/includes/sessions.php

    r8647 r8711  
    112112        ),
    113113        [
     114            'align'                => get_shared_definition( 'align_block', 'attribute' ),
    114115            'className'            => get_shared_definition( 'string_empty', 'attribute' ),
    115116            'featured_image_width' => array(
     
    217218 */
    218219function get_session_posts( array $attributes ) {
     220    if ( empty( $attributes['mode'] ) ) {
     221        return [];
     222    }
     223
    219224    $post_args = [
    220225        'post_type'      => 'wcb_session',
Note: See TracChangeset for help on using the changeset viewer.