Making WordPress.org

Changeset 8712


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

WordCamp Blocks: Refactor Speakers 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:
8 edited

Legend:

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

    r8647 r8712  
    22 * External dependencies
    33 */
    4 import { get } from 'lodash';
     4import { get }    from 'lodash';
    55import classnames from 'classnames';
    66
     
    88 * WordPress dependencies
    99 */
    10 const { Disabled } = wp.components;
     10const { Disabled }            = wp.components;
    1111const { Component, Fragment } = wp.element;
    12 const { __, _n } = wp.i18n;
    13 const { escapeAttribute } = wp.escapeHtml;
     12const { __, _n }              = wp.i18n;
     13const { escapeAttribute }     = wp.escapeHtml;
    1414
    1515/**
    1616 * Internal dependencies
    1717 */
    18 import { AvatarImage } from '../shared/avatar';
    19 import { ItemTitle, ItemHTMLContent, ItemPermalink } from '../shared/block-content';
    20 import { tokenSplit, arrayTokenReplace } from '../shared/i18n';
    21 import GridContentLayout from '../shared/grid-layout/block-content';
     18import { AvatarImage }                                               from '../shared/avatar';
     19import { ItemTitle, ItemHTMLContent, ItemPermalink, BlockNoContent } from '../shared/block-content';
     20import { tokenSplit, arrayTokenReplace }                             from '../shared/i18n';
     21import GridContentLayout                                             from '../shared/grid-layout/block-content';
     22import { filterEntities }                                            from '../blocks-store';
     23
    2224import './block-content.scss';
    2325
     26/**
     27 * Component for the section of each speaker post that displays information about relevant sessions.
     28 *
     29 * @param {Object} props {
     30 *     @type {Object} speaker
     31 *     @type {Array}  tracks
     32 * }
     33 *
     34 * @return {Element}
     35 */
    2436function SpeakerSessions( { speaker, tracks } ) {
    2537    const sessions = get( speaker, '_embedded.sessions', [] );
     
    8496}
    8597
     98/**
     99 * Component for displaying the block content.
     100 */
    86101class SpeakersBlockContent extends Component {
     102    /**
     103     * Run additional operations during component initialization.
     104     *
     105     * @param {Object} props
     106     */
     107    constructor( props ) {
     108        super( props );
     109
     110        this.getFilteredPosts = this.getFilteredPosts.bind( this );
     111    }
     112
     113    /**
     114     * Filter and sort the content that will be rendered.
     115     *
     116     * @returns {Array}
     117     */
     118    getFilteredPosts() {
     119        const { attributes, entities } = this.props;
     120        const { wcb_speaker: posts } = entities;
     121        const { mode, item_ids, sort } = attributes;
     122
     123        const args = {};
     124
     125        if ( Array.isArray( item_ids ) && item_ids.length > 0 ) {
     126            args.filter  = [
     127                {
     128                    fieldName  : mode === 'wcb_speaker' ? 'id' : 'speaker_group',
     129                    fieldValue : item_ids,
     130                },
     131            ];
     132        }
     133
     134        args.sort = sort;
     135
     136        return filterEntities( posts, args );
     137    }
     138
     139    /**
     140     * Render the block content.
     141     *
     142     * @return {Element}
     143     */
    87144    render() {
    88         const { attributes, speakerPosts, tracks } = this.props;
    89         const {
    90             show_avatars, avatar_size, avatar_align,
    91             content, show_session,
    92         } = attributes;
     145        const { attributes, entities } = this.props;
     146        const { wcb_track: tracks } = entities;
     147        const { show_avatars, avatar_size, avatar_align, content, show_session } = attributes;
     148
     149        const posts     = this.getFilteredPosts();
     150        const isLoading = ! Array.isArray( posts );
     151        const hasPosts  = ! isLoading && posts.length > 0;
     152
     153        if ( isLoading || ! hasPosts ) {
     154            return (
     155                <BlockNoContent loading={ isLoading } />
     156            );
     157        }
    93158
    94159        return (
     
    97162                { ...this.props }
    98163            >
    99                 { speakerPosts.map( ( post ) =>
     164                { posts.map( ( post ) =>
    100165                    <div
    101166                        key={ post.slug }
     
    142207                            />
    143208                        }
    144                     </div>,
     209                    </div>
    145210                ) }
    146211            </GridContentLayout>
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/speakers/block-controls.js

    r8526 r8712  
    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 SpeakersBlockContent from './block-content';
    17 import SpeakersSelect from './speakers-select';
    18 import { LABEL }                                                        from './index';
     15import { BlockControls, PlaceholderSpecificMode } from '../shared/block-controls';
     16import SpeakersBlockContent                       from './block-content';
     17import SpeakersSelect                             from './speakers-select';
     18import { LABEL }                                  from './index';
    1919
     20/**
     21 * Component for displaying a UI within the block.
     22 */
    2023class SpeakersBlockControls extends BlockControls {
     24    /**
     25     * Render the internal block UI.
     26     *
     27     * @return {Element}
     28     */
    2129    render() {
    22         const { icon, attributes, setAttributes, speakerPosts } = this.props;
     30        const { icon, attributes, setAttributes } = this.props;
    2331        const { mode } = attributes;
    24 
    25         const hasPosts = Array.isArray( speakerPosts ) && speakerPosts.length;
    26 
    27         if ( mode && ! hasPosts ) {
    28             return (
    29                 <PlaceholderNoContent
    30                     icon={ icon }
    31                     label={ LABEL }
    32                     loading={ ! Array.isArray( speakerPosts ) }
    33                 />
    34             );
    35         }
    3632
    3733        let output;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/speakers/edit.js

    r8611 r8712  
    1 /**
    2  * External dependencies
    3  */
    4 import { isUndefined, pickBy, split } from 'lodash';
    5 
    61/**
    72 * WordPress dependencies
    83 */
    9 const apiFetch = wp.apiFetch;
    104const { withSelect } = wp.data;
    115const { Component, Fragment } = wp.element;
    12 const { addQueryArgs } = wp.url;
    136
    147/**
    158 * Internal dependencies
    169 */
    17 import SpeakersBlockControls from './block-controls';
     10import SpeakersBlockControls     from './block-controls';
    1811import SpeakersInspectorControls from './inspector-controls';
    19 import SpeakersToolbar from './toolbar';
    20 import { ICON }        from './index';
     12import SpeakersToolbar           from './toolbar';
     13import { ICON }                  from './index';
     14import { WC_BLOCKS_STORE }       from '../blocks-store';
    2115
    2216const blockData = window.WordCampBlocks.speakers || {};
    2317
    24 const MAX_POSTS = 100;
    25 
    26 const ALL_POSTS_QUERY = {
    27     orderby  : 'title',
    28     order    : 'asc',
    29     per_page : MAX_POSTS,
    30     _embed   : true,
    31 };
    32 
    33 const ALL_TERMS_QUERY = {
    34     orderby  : 'name',
    35     order    : 'asc',
    36     per_page : MAX_POSTS,
    37 };
    38 
     18/**
     19 * Top-level component for the editing UI for the block.
     20 */
    3921class SpeakersEdit extends Component {
    40     constructor( props ) {
    41         super( props );
    42 
    43         this.fetchSpeakers();
    44     }
    45 
    46     fetchSpeakers() {
    47         const allSpeakerPosts = apiFetch( {
    48             path: addQueryArgs( `/wp/v2/speakers`, ALL_POSTS_QUERY ),
    49         } );
    50         const allSpeakerTerms = apiFetch( {
    51             path: addQueryArgs( `/wp/v2/speaker_group`, ALL_TERMS_QUERY ),
    52         } );
    53 
    54         this.state = {
    55             allSpeakerPosts : allSpeakerPosts, // Promise
    56             allSpeakerTerms : allSpeakerTerms, // Promise
    57         }
    58     }
    59 
     22    /**
     23     * Render the block's editing UI.
     24     *
     25     * @return {Element}
     26     */
    6027    render() {
    6128        const { mode } = this.props.attributes;
     
    6633                    icon={ ICON }
    6734                    { ...this.props }
    68                     { ...this.state }
    6935                />
    7036                { mode &&
     
    7945}
    8046
    81 const speakersSelect = ( select, props ) => {
    82     const { mode, item_ids, sort } = props.attributes;
    83     const { getEntityRecords } = select( 'core' );
    84     const [ orderby, order ] = split( sort, '_', 2 );
     47const speakersSelect = ( select ) => {
     48    const { getEntities } = select( WC_BLOCKS_STORE );
    8549
    86     const args = {
    87         orderby  : orderby,
    88         order    : order,
    89         per_page : MAX_POSTS, // -1 is not allowed for per_page.
    90         _embed   : true,
    91         context  : 'view',
     50    const entities = {
     51        wcb_speaker       : getEntities( 'postType', 'wcb_speaker', { _embed: true } ),
     52        wcb_speaker_group : getEntities( 'taxonomy', 'wcb_speaker_group' ),
     53        wcb_track         : getEntities( 'taxonomy', 'wcb_track' ),
    9254    };
    9355
    94     if ( Array.isArray( item_ids ) ) {
    95         switch ( mode ) {
    96             case 'wcb_speaker':
    97                 args.include = item_ids;
    98                 break;
    99             case 'wcb_speaker_group':
    100                 args.speaker_group = item_ids;
    101                 break;
    102         }
    103     }
    104 
    105     const speakersQuery = pickBy( args, ( value ) => ! isUndefined( value ) );
    106 
    10756    return {
    108         blockData    : blockData,
    109         speakerPosts : getEntityRecords( 'postType', 'wcb_speaker', speakersQuery ),
    110         tracks       : getEntityRecords( 'taxonomy', 'wcb_track', { per_page: MAX_POSTS } ),
     57        blockData,
     58        entities,
    11159    };
    11260};
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/speakers/index.js

    r8627 r8712  
    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/speakers/inspector-controls.js

    r8647 r8712  
    3333};
    3434
     35/**
     36 * Component for block controls that appear in the Inspector Panel.
     37 */
    3538class SpeakerInspectorControls extends Component {
     39    /**
     40     * Render the controls.
     41     *
     42     * @return {Element}
     43     */
    3644    render() {
    3745        const { attributes, setAttributes, blockData } = this.props;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/speakers/speakers-select.js

    r8633 r8712  
    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 { AvatarImage } from '../shared/avatar';
    17 import ItemSelect from '../shared/item-select';
     15import ItemSelect, { buildOptions, Option } from '../shared/item-select';
    1816
     17/**
     18 * Component for selecting posts/terms for populating the block content.
     19 */
    1920class SpeakersSelect extends Component {
     21    /**
     22     * Run additional operations during component initialization.
     23     *
     24     * @param {Object} props
     25     */
    2026    constructor( props ) {
    2127        super( props );
    2228
    23         this.state = {
    24             wcb_speaker       : [],
    25             wcb_speaker_group : [],
    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 { allSpeakerPosts, allSpeakerTerms } = props;
     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_speaker, wcb_speaker_group } = entities;
    3542
    36         const parsedPosts = allSpeakerPosts.then(
    37             ( fetchedPosts ) => {
    38                 const posts = fetchedPosts.map( ( post ) => {
    39                     return {
    40                         label  : post.title.rendered.trim() || __( '(Untitled)', 'wordcamporg' ),
    41                         value  : post.id,
    42                         type   : 'wcb_speaker',
    43                         avatar : post.avatar_urls[ '24' ],
    44                     };
    45                 } );
     43        const optionGroups = [
     44            {
     45                entityType : 'post',
     46                type       : 'wcb_speaker',
     47                label      : __( 'Speakers', 'wordcamporg' ),
     48                items      : wcb_speaker,
     49            },
     50            {
     51                entityType : 'term',
     52                type       : 'wcb_speaker_group',
     53                label      : __( 'Groups', 'wordcamporg' ),
     54                items      : wcb_speaker_group,
     55            },
     56        ];
    4657
    47                 this.setState( { wcb_speaker: posts } );
    48             }
    49         );
    50 
    51         const parsedTerms = allSpeakerTerms.then(
    52             ( fetchedTerms ) => {
    53                 const terms = fetchedTerms.map( ( term ) => {
    54                     return {
    55                         label : term.name || __( '(Untitled)', 'wordcamporg' ),
    56                         value : term.id,
    57                         type  : 'wcb_speaker_group',
    58                         count : term.count,
    59                     };
    60                 } );
    61 
    62                 this.setState( { wcb_speaker_group: terms } );
    63             }
    64         );
    65 
    66         Promise.all( [ parsedPosts, parsedTerms ] ).then( () => {
    67             this.setState( { loading: false } );
    68         } );
     58        return buildOptions( optionGroups );
    6959    }
    7060
    71     buildSelectOptions( mode ) {
    72         const { getOwnPropertyDescriptors } = Object;
    73         const options = [];
     61    /**
     62     * Determine the currently selected options in the Select dropdown based on block attributes.
     63     *
     64     * @return {Array}
     65     */
     66    getCurrentSelectValue() {
     67        const { attributes } = this.props;
     68        const { mode, item_ids } = attributes;
    7469
    75         const labels = {
    76             wcb_speaker       : __( 'Speakers', 'wordcamporg' ),
    77             wcb_speaker_group : __( 'Groups', 'wordcamporg' ),
    78         };
    79 
    80         for ( const type in getOwnPropertyDescriptors( this.state ) ) {
    81             if ( ( ! mode || type === mode ) && this.state[ type ].length ) {
    82                 options.push( {
    83                     label   : labels[ type ],
    84                     options : this.state[ type ],
    85                 } );
    86             }
    87         }
    88 
    89         return options;
    90     }
    91 
    92     render() {
    93         const { label, icon, attributes, setAttributes } = this.props;
    94         const { mode, item_ids } = attributes;
    95         const options = this.buildSelectOptions( mode );
     70        const options = flatMap( this.buildSelectOptions(), ( group ) => {
     71            return group.options;
     72        } );
    9673
    9774        let value = [];
    9875
    9976        if ( mode && item_ids.length ) {
    100             const modeOptions = get( options, '[0].options', [] );
    101 
    102             value = modeOptions.filter( ( option ) => {
    103                 return includes( item_ids, option.value );
     77            value = options.filter( ( option ) => {
     78                return mode === option.type && includes( item_ids, option.value );
    10479            } );
    10580        }
     81
     82        return value;
     83    }
     84
     85    /**
     86     * Check if all of the entity groups have finished loading.
     87     *
     88     * @return {boolean}
     89     */
     90    isLoading() {
     91        const { entities } = this.props;
     92
     93        return ! every( entities, ( value ) => {
     94            return Array.isArray( value );
     95        } );
     96    }
     97
     98    /**
     99     * Render an ItemSelect component with block-specific settings.
     100     *
     101     * @return {Element}
     102     */
     103    render() {
     104        const { label, icon, setAttributes } = this.props;
    106105
    107106        return (
     
    109108                className="wordcamp-speakers-select"
    110109                label={ label }
    111                 value={ value }
    112                 buildSelectOptions={ this.buildSelectOptions }
     110                value={ this.getCurrentSelectValue() }
    113111                onChange={ ( changed ) => setAttributes( changed ) }
    114                 mode={ mode }
    115112                selectProps={ {
    116                     isLoading        : this.state.loading,
    117                     formatGroupLabel : ( groupData ) => {
     113                    options           : this.buildSelectOptions(),
     114                    isLoading         : this.isLoading(),
     115                    formatOptionLabel : ( optionData ) => {
    118116                        return (
    119                             <span className="wordcamp-item-select-option-group-label">
    120                                 { groupData.label }
    121                             </span>
    122                         );
    123                     },
    124                     formatOptionLabel: ( optionData ) => {
    125                         return (
    126                             <SpeakersOption
    127                                 icon={ icon }
     117                            <Option
     118                                icon={ 'wcb_speaker_group' === optionData.type ? icon : null }
    128119                                { ...optionData }
    129120                            />
     
    136127}
    137128
    138 function SpeakersOption( { type, icon, label = '', avatar = '', count = 0 } ) {
    139     let image, content;
    140 
    141     switch ( type ) {
    142         case 'wcb_speaker' :
    143             image = (
    144                 <AvatarImage
    145                     className="wordcamp-item-select-option-avatar"
    146                     name={ label }
    147                     size={ 24 }
    148                     url={ avatar }
    149                 />
    150             );
    151             content = (
    152                 <span className="wordcamp-item-select-option-label">
    153                     { label }
    154                 </span>
    155             );
    156             break;
    157 
    158         case 'wcb_speaker_group' :
    159             image = (
    160                 <div className="wordcamp-item-select-option-icon-container">
    161                     <Dashicon
    162                         className="wordcamp-item-select-option-icon"
    163                         icon={ icon }
    164                         size={ 16 }
    165                     />
    166                 </div>
    167             );
    168             content = (
    169                 <span className="wordcamp-item-select-option-label">
    170                     { label }
    171                     <span className="wordcamp-item-select-option-label-term-count">
    172                         { count }
    173                     </span>
    174                 </span>
    175             );
    176             break;
    177     }
    178 
    179     return (
    180         <div className="wordcamp-item-select-option">
    181             { image }
    182             { content }
    183         </div>
    184     );
    185 }
    186 
    187129export default SpeakersSelect;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/speakers/toolbar.js

    r8611 r8712  
    66const { Component }     = wp.element;
    77
     8/**
     9 * Component for adding UI buttons to the top of the block.
     10 */
    811class SpeakersToolbar extends Component {
     12    /**
     13     * Render the toolbar.
     14     *
     15     * @return {Element}
     16     */
    917    render() {
    1018        const { attributes, setAttributes, blockData } = this.props;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/includes/speakers.php

    r8647 r8712  
    9595 */
    9696function get_speaker_posts( array $attributes ) {
     97    if ( empty( $attributes['mode'] ) ) {
     98        return [];
     99    }
     100
    97101    $post_args = [
    98102        'post_type'      => 'wcb_speaker',
Note: See TracChangeset for help on using the changeset viewer.