Making WordPress.org

Changeset 8710


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

WordCamp Blocks: Refactor Organizers 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/organizers/block-content.js

    r8647 r8710  
    88 */
    99const { Component } = wp.element;
    10 const { __ }        = wp.i18n;
    1110
    1211/**
    1312 * Internal dependencies
    1413 */
    15 import { AvatarImage }                from '../shared/avatar';
    16 import {ItemTitle, ItemHTMLContent, ItemPermalink} from '../shared/block-content';
    17 import GridContentLayout              from '../shared/grid-layout/block-content';
     14import { AvatarImage }                                from '../shared/avatar';
     15import { BlockNoContent, ItemTitle, ItemHTMLContent } from '../shared/block-content';
     16import GridContentLayout                              from '../shared/grid-layout/block-content';
     17import { filterEntities }                             from '../blocks-store';
    1818
     19/**
     20 * Component for displaying the block content.
     21 */
    1922class OrganizersBlockContent extends Component {
     23    /**
     24     * Run additional operations during component initialization.
     25     *
     26     * @param {Object} props
     27     */
     28    constructor( props ) {
     29        super( props );
     30
     31        this.getFilteredPosts = this.getFilteredPosts.bind( this );
     32    }
     33
     34    /**
     35     * Filter and sort the content that will be rendered.
     36     *
     37     * @returns {Array}
     38     */
     39    getFilteredPosts() {
     40        const { attributes, entities } = this.props;
     41        const { wcb_organizer: posts } = entities;
     42        const { mode, item_ids, sort } = attributes;
     43
     44        const args = {};
     45
     46        if ( Array.isArray( item_ids ) && item_ids.length > 0 ) {
     47            args.filter = [
     48                {
     49                    fieldName  : mode === 'wcb_organizer' ? 'id' : 'organizer_team',
     50                    fieldValue : item_ids,
     51                },
     52            ];
     53        }
     54
     55        args.sort = sort;
     56
     57        return filterEntities( posts, args );
     58    }
     59
     60    /**
     61     * Render the block content.
     62     *
     63     * @return {Element}
     64     */
    2065    render() {
    21         const { attributes, organizerPosts }                                    = this.props;
     66        const { attributes } = this.props;
    2267        const { show_avatars, avatar_size, avatar_align, content } = attributes;
     68
     69        const posts     = this.getFilteredPosts();
     70        const isLoading = ! Array.isArray( posts );
     71        const hasPosts  = ! isLoading && posts.length > 0;
     72
     73        if ( isLoading || ! hasPosts ) {
     74            return (
     75                <BlockNoContent loading={ isLoading } />
     76            );
     77        }
    2378
    2479        return (
     
    2782                { ...this.props }
    2883            >
    29                 { organizerPosts.map( ( post ) => /* Note that organizer posts are not 'public', so there are no permalinks. */
     84                { posts.map( ( post ) => /* Note that organizer posts are not 'public', so there are no permalinks. */
    3085                    <div
    3186                        key={ post.slug }
     
    56111                            />
    57112                        }
    58                     </div>,
     113                    </div>
    59114                ) }
    60115            </GridContentLayout>
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/organizers/block-controls.js

    r8566 r8710  
    1313 * Internal dependencies
    1414 */
    15 import { BlockControls, PlaceholderNoContent, PlaceholderSpecificMode } from '../shared/block-controls';
    16 import OrganizersBlockContent                                           from './block-content';
    17 import OrganizersSelect                                                 from './organizers-select';
    18 import { LABEL }                                                        from './index';
     15import { BlockControls, PlaceholderSpecificMode } from '../shared/block-controls';
     16import OrganizersBlockContent                     from './block-content';
     17import OrganizersSelect                           from './organizers-select';
     18import { LABEL }                                  from './index';
    1919
     20/**
     21 * Component for displaying a UI within the block.
     22 */
    2023class OrganizersBlockControls extends BlockControls {
     24    /**
     25     * Render the internal block UI.
     26     *
     27     * @return {Element}
     28     */
    2129    render() {
    22         const { icon, attributes, setAttributes, organizerPosts } = this.props;
    23         const { mode }                                            = attributes;
    24 
    25         const hasPosts = Array.isArray( organizerPosts ) && organizerPosts.length;
    26 
    27         if ( mode && ! hasPosts ) {
    28             return (
    29                 <PlaceholderNoContent
    30                     icon={ icon }
    31                     label={ LABEL }
    32                     loading={ ! Array.isArray( organizerPosts ) }
    33                 />
    34             );
    35         }
     30        const { icon, attributes, setAttributes } = this.props;
     31        const { mode } = attributes;
    3632
    3733        let output;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/organizers/edit.js

    r8611 r8710  
    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/**
     
    1912import OrganizersToolbar           from './toolbar';
    2013import { ICON }                    from './index';
     14import { WC_BLOCKS_STORE }         from '../blocks-store';
    2115
    2216const blockData = window.WordCampBlocks.organizers || {};
    23 const MAX_POSTS = 100;
    2417
    25 const ALL_POSTS_QUERY = {
    26     orderby  : 'title',
    27     order    : 'asc',
    28     per_page : MAX_POSTS,
    29 };
    30 
    31 const ALL_TERMS_QUERY = {
    32     orderby  : 'name',
    33     order    : 'asc',
    34     per_page : MAX_POSTS,
    35 };
    36 
     18/**
     19 * Top-level component for the editing UI for the block.
     20 */
    3721class OrganizersEdit extends Component {
    38     constructor( props ) {
    39         super( props );
    40 
    41         this.state = {
    42             allOrganizerPosts : null,
    43             allOrganizerTerms : null,
    44         };
    45 
    46         this.fetchOrganizerDetails();
    47     }
    48 
    49     fetchOrganizerDetails() {
    50         const allOrganizerPosts = apiFetch( {
    51             path: addQueryArgs( '/wp/v2/organizers', ALL_POSTS_QUERY ),
    52         } );
    53 
    54         const allOrganizerTerms = apiFetch( {
    55             path: addQueryArgs( '/wp/v2/organizer_team', ALL_TERMS_QUERY ),
    56         } );
    57 
    58         this.state = {
    59             allOrganizerPosts : allOrganizerPosts, // Promise
    60             allOrganizerTerms : allOrganizerTerms, // Promise
    61         }
    62     }
    63 
     22    /**
     23     * Render the block's editing UI.
     24     *
     25     * @return {Element}
     26     */
    6427    render() {
    6528        const { mode } = this.props.attributes;
     
    7033                    icon={ ICON }
    7134                    { ...this.props }
    72                     { ...this.state }
    7335                />
    7436
     
    8446}
    8547
    86 const organizerSelect = ( select, props ) => {
    87     const { mode, item_ids, sort } = props.attributes;
    88     const { getEntityRecords }     = select( 'core' );
    89     const [ orderby, order ]       = split( sort, '_', 2 );
     48const organizerSelect = ( select ) => {
     49    const { getEntities } = select( WC_BLOCKS_STORE );
    9050
    91     const args = {
    92         orderby  : orderby,
    93         order    : order,
    94         per_page : MAX_POSTS, // -1 is not allowed for per_page.
    95         context  : 'view',
     51    const entities = {
     52        wcb_organizer      : getEntities( 'postType', 'wcb_organizer', { _embed: true } ),
     53        wcb_organizer_team : getEntities( 'taxonomy', 'wcb_organizer_team' ),
    9654    };
    9755
    98     if ( Array.isArray( item_ids ) ) {
    99         switch ( mode ) {
    100             case 'wcb_organizer':
    101                 args.include = item_ids;
    102                 break;
    103             case 'wcb_organizer_team':
    104                 args.organizer_team = item_ids;
    105                 break;
    106         }
    107     }
    108 
    109     const organizersQuery = pickBy(
    110         args,
    111         ( value ) => ! isUndefined( value )
    112     );
    113 
    11456    return {
    115         blockData      : blockData,
    116         organizerPosts : getEntityRecords( 'postType', 'wcb_organizer', organizersQuery ),
     57        blockData,
     58        entities,
    11759    };
    11860};
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/organizers/index.js

    r8627 r8710  
    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/organizers/inspector-controls.js

    r8647 r8710  
    3434};
    3535
     36/**
     37 * Component for block controls that appear in the Inspector Panel.
     38 */
    3639class OrganizerInspectorControls extends Component {
     40    /**
     41     * Render the controls.
     42     *
     43     * @return {Element}
     44     */
    3745    render() {
    38         const { attributes, setAttributes, blockData }                                 = this.props;
     46        const { attributes, setAttributes, blockData }                   = this.props;
    3947        const { show_avatars, avatar_size, avatar_align, content, sort } = attributes;
    40         const { schema = DEFAULT_SCHEMA, options = DEFAULT_OPTIONS }                   = blockData;
     48        const { schema = DEFAULT_SCHEMA, options = DEFAULT_OPTIONS }     = blockData;
    4149
    4250        return (
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/organizers/organizers-select.js

    r8633 r8710  
    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;
    1110const { __ }        = wp.i18n;
     
    1413 * Internal dependencies
    1514 */
    16 import { AvatarImage } from '../shared/avatar';
    17 import ItemSelect  from '../shared/item-select';
    18 import { ICON }    from './index';
     15import ItemSelect, { buildOptions, Option } from '../shared/item-select';
    1916
     17/**
     18 * Component for selecting posts/terms for populating the block content.
     19 */
    2020class OrganizersSelect extends Component {
     21    /**
     22     * Run additional operations during component initialization.
     23     *
     24     * @param {Object} props
     25     */
    2126    constructor( props ) {
    2227        super( props );
    2328
    24         this.state = {
    25             wcb_organizer      : [],
    26             wcb_organizer_team : [],
    27             loading            : true,
    28         };
    29 
    30         this.buildSelectOptions = this.buildSelectOptions.bind( this );
    31         this.fetchSelectOptions( props );
     29        this.buildSelectOptions    = this.buildSelectOptions.bind( this );
     30        this.getCurrentSelectValue = this.getCurrentSelectValue.bind( this );
     31        this.isLoading             = this.isLoading.bind( this );
    3232    }
    3333
    34     fetchSelectOptions( props ) {
    35         const { allOrganizerPosts, allOrganizerTerms } = 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_organizer, wcb_organizer_team } = entities;
    3642
    37         const parsedPosts = allOrganizerPosts.then(
    38             ( fetchedPosts ) => {
    39                 const posts = fetchedPosts.map( ( post ) => {
    40                     return {
    41                         label  : post.title.rendered.trim() || __( '(Untitled)', 'wordcamporg' ),
    42                         value  : post.id,
    43                         type   : 'wcb_organizer',
    44                         avatar : post.avatar_urls[ '24' ],
    45                     };
    46                 } );
     43        const optionGroups = [
     44            {
     45                entityType : 'post',
     46                type       : 'wcb_organizer',
     47                label      : __( 'Organizers', 'wordcamporg' ),
     48                items      : wcb_organizer,
     49            },
     50            {
     51                entityType : 'term',
     52                type       : 'wcb_organizer_team',
     53                label      : __( 'Teams', 'wordcamporg' ),
     54                items      : wcb_organizer_team,
     55            },
     56        ];
    4757
    48                 this.setState( { wcb_organizer: posts } );
    49             }
    50         );
    51 
    52         const parsedTerms = allOrganizerTerms.then(
    53             ( fetchedTerms ) => {
    54                 const terms = fetchedTerms.map( ( term ) => {
    55                     return {
    56                         label : term.name.trim() || __( '(Untitled)', 'wordcamporg' ),
    57                         value : term.id,
    58                         type  : 'wcb_organizer_team',
    59                         count : term.count,
    60                     };
    61                 } );
    62 
    63                 this.setState( { wcb_organizer_team: terms } );
    64             }
    65         );
    66 
    67         Promise.all( [ parsedPosts, parsedTerms ] ).then( () => {
    68             this.setState( { loading: false } );
    69         } );
     58        return buildOptions( optionGroups );
    7059    }
    7160
    72     buildSelectOptions( mode ) {
    73         const { getOwnPropertyDescriptors } = Object;
    74         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;
    7569
    76         const labels = {
    77             wcb_organizer      : __( 'Organizers', 'wordcamporg' ),
    78             wcb_organizer_team : __( 'Teams',      'wordcamporg' ),
    79         };
    80 
    81         for ( const type in getOwnPropertyDescriptors( this.state ) ) {
    82             if ( ! this.state[ type ].length ) {
    83                 continue;
    84             }
    85 
    86             if ( mode && type !== mode ) {
    87                 continue;
    88             }
    89 
    90             options.push( {
    91                 label   : labels[ type ],
    92                 options : this.state[ type ],
    93             } );
    94         }
    95 
    96         return options;
    97     }
    98 
    99     render() {
    100         const { label, attributes, setAttributes } = this.props;
    101         const { mode, item_ids }                   = attributes;
    102         const options                              = this.buildSelectOptions( mode );
     70        const options = flatMap( this.buildSelectOptions(), ( group ) => {
     71            return group.options;
     72        } );
    10373
    10474        let value = [];
    10575
    10676        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 );
     77            value = options.filter( ( option ) => {
     78                return mode === option.type && includes( item_ids, option.value );
    11179            } );
    11280        }
    11381
     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, setAttributes } = this.props;
     105
    114106        return (
    115107            <ItemSelect
    116                 className="wordcamp-organizer-select"
     108                className="wordcamp-organizers-select"
    117109                label={ label }
    118                 value={ value }
    119                 buildSelectOptions={ this.buildSelectOptions }
     110                value={ this.getCurrentSelectValue() }
    120111                onChange={ ( changed ) => setAttributes( changed ) }
    121                 mode={ mode }
    122112                selectProps={ {
    123                     isLoading        : this.state.loading,
    124                     formatGroupLabel : ( groupData ) => {
     113                    options           : this.buildSelectOptions(),
     114                    isLoading         : this.isLoading(),
     115                    formatOptionLabel : ( optionData ) => {
    125116                        return (
    126                             <span className="wordcamp-item-select-option-group-label">
    127                                 { groupData.label }
    128                             </span>
    129                         );
    130                     },
    131                     formatOptionLabel: ( optionData ) => {
    132                         return (
    133                             <OrganizersOption { ...optionData } />
     117                            <Option { ...optionData } />
    134118                        );
    135119                    },
     
    140124}
    141125
    142 function OrganizersOption( { type, label = '', avatar = '', count = 0 } ) {
    143     let image, content;
    144 
    145     switch ( type ) {
    146         case 'wcb_organizer' :
    147             image = (
    148                 <AvatarImage
    149                     className="wordcamp-item-select-option-avatar"
    150                     name={ label }
    151                     size={ 24 }
    152                     url={ avatar }
    153                 />
    154             );
    155             content = (
    156                 <span className="wordcamp-item-select-option-label">
    157                     { label }
    158                 </span>
    159             );
    160             break;
    161 
    162         case 'wcb_organizer_team' :
    163             image = (
    164                 <div className="wordcamp-item-select-option-icon-container">
    165                     <Dashicon
    166                         className="wordcamp-item-select-option-icon"
    167                         icon={ ICON }
    168                         size={ 16 }
    169                     />
    170                 </div>
    171             );
    172             content = (
    173                 <span className="wordcamp-item-select-option-label">
    174                     { label }
    175                     <span className="wordcamp-item-select-option-label-term-count">
    176                         { count }
    177                     </span>
    178                 </span>
    179             );
    180             break;
    181     }
    182 
    183     return (
    184         <div className="wordcamp-item-select-option">
    185             { image }
    186             { content }
    187         </div>
    188     );
    189 }
    190 
    191126export default OrganizersSelect;
  • sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins/blocks/assets/src/organizers/toolbar.js

    r8633 r8710  
    66const { Component }      = wp.element;
    77
     8/**
     9 * Component for adding UI buttons to the top of the block.
     10 */
    811class OrganizersToolbar 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/organizers.php

    r8647 r8710  
    9191 */
    9292function get_organizer_posts( array $attributes ) {
     93    if ( empty( $attributes['mode'] ) ) {
     94        return [];
     95    }
     96
    9397    $post_args = [
    9498        'post_type'      => 'wcb_organizer',
Note: See TracChangeset for help on using the changeset viewer.