Skip to content
Merged
2 changes: 1 addition & 1 deletion build/content-helper/editor-sidebar.asset.php
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => 'c3e7dc1ccdd9065ce0db');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom-ready', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-wordcount'), 'version' => 'd42794a0110c4ff80005');
24 changes: 12 additions & 12 deletions build/content-helper/editor-sidebar.js

Large diffs are not rendered by default.

120 changes: 80 additions & 40 deletions src/content-helper/editor-sidebar/related-posts/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SelectControl,
} from '@wordpress/components';
import { useDebounce } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

Expand All @@ -33,6 +33,7 @@ import { RelatedPostItem } from './component-item';
import { usePostData } from './hooks';
import { RelatedPostsProvider } from './provider';
import './related-posts.scss';
import { RelatedPostsStore } from './store';

const FETCH_RETRIES = 1;

Expand Down Expand Up @@ -139,14 +140,29 @@ export const RelatedPostsPanel = (): React.JSX.Element => {
} );
}, [ authors, categories, tags, isPostDataReady ] );

const [ loading, setLoading ] = useState<boolean>( true );
const {
firstRun,
loading,
posts,
filters,
} = useSelect( ( select ) => {
const { isLoading, getPosts, getFilters, isFirstRun } = select( RelatedPostsStore );
return {
firstRun: isFirstRun(),
loading: isLoading(),
posts: getPosts(),
filters: getFilters(),
};
}, [] );

const {
setFirstRun,
setLoading,
setPosts,
setFilters,
} = useDispatch( RelatedPostsStore );

const [ error, setError ] = useState<ContentHelperError>();
const [ posts, setPosts ] = useState<PostData[]>( [] );
const [ filters, setFilters ] = useState<PostFilters>( {
author: '',
section: '',
tags: [],
} );

const [ postContent, setPostContent ] = useState<string|undefined>( undefined );
const debouncedSetPostContent = useDebounce( setPostContent, 1000 );
Expand All @@ -170,13 +186,17 @@ export const RelatedPostsPanel = (): React.JSX.Element => {
*/
const onMetricChange = ( selection: string ) => {
if ( isInEnum( selection, Metric ) ) {
const updatedMetric = selection as Metric;

setSettings( {
RelatedPosts: {
...settings.RelatedPosts,
Metric: selection as Metric,
Metric: updatedMetric,
},
} );
Telemetry.trackEvent( 'related_posts_metric_changed', { metric: selection } );
Telemetry.trackEvent( 'related_posts_metric_changed', { metric: updatedMetric } );

fetchPosts( period, updatedMetric, filters, FETCH_RETRIES );
}
};

Expand All @@ -189,43 +209,58 @@ export const RelatedPostsPanel = (): React.JSX.Element => {
*/
const onPeriodChange = ( selection: string ) => {
if ( isInEnum( selection, Period ) ) {
const updatedPeriod = selection as Period;

setSettings( {
RelatedPosts: {
...settings.RelatedPosts,
Period: selection as Period,
Period: updatedPeriod,
},
} );
Telemetry.trackEvent( 'related_posts_period_changed', { period: selection } );
Telemetry.trackEvent( 'related_posts_period_changed', { period: updatedPeriod } );

fetchPosts( updatedPeriod, metric, filters, FETCH_RETRIES );
}
};

useEffect( () => {
const fetchPosts = async ( retries: number ) => {
RelatedPostsProvider.getInstance().getRelatedPosts( period, metric, filters )
.then( ( result ): void => {
setPosts( result );
setLoading( false );
} )
.catch( async ( err ) => {
if ( retries > 0 && err.retryFetch ) {
await new Promise( ( r ) => setTimeout( r, 500 ) );
await fetchPosts( retries - 1 );
} else {
setLoading( false );
setError( err );
}
} );
};

/**
* Fetches the related posts.
*
* @since 3.4.0
* @since 3.18.0 Added `fetchPeriod`, `fetchMetric`, and `fetchFilters` parameters.
*
* @param {Period} fetchPeriod The period for which to fetch data.
* @param {Metric} fetchMetric The metric to sort by.
* @param {PostFilters} fetchFilters The filters to use in the request.
* @param {number} retries The max number of retries to perform in case of failure(s).
*/
const fetchPosts = async (
fetchPeriod: Period, fetchMetric: Metric, fetchFilters: PostFilters, retries: number
) : Promise<void> => {
setLoading( true );
fetchPosts( FETCH_RETRIES );

return (): void => {
setLoading( false );
setPosts( [] );
setError( undefined );
};
}, [ period, metric, filters, postData ] );
RelatedPostsProvider.getInstance().getRelatedPosts( fetchPeriod, fetchMetric, fetchFilters )
.then( ( result ): void => {
setPosts( result );
setLoading( false );
} )
.catch( async ( err ) => {
if ( retries > 0 && err.retryFetch ) {
await new Promise( ( r ) => setTimeout( r, 500 ) );
await fetchPosts( fetchPeriod, fetchMetric, fetchFilters, retries - 1 );
} else {
setLoading( false );
setError( err );
setPosts( [] );
}
} );
};

if ( firstRun ) {
// Run initial fetch when the component is mounted.
fetchPosts( period, metric, filters, FETCH_RETRIES );
setFirstRun( false );
}

/**
* Updates the filters value.
Expand All @@ -243,17 +278,22 @@ export const RelatedPostsPanel = (): React.JSX.Element => {
newValue = '';
}

let updatedFilters;

if ( PostFilterType.Tag === filterType ) {
let values: string[] = [];

if ( '' !== newValue ) {
values = newValue.split( ',' ).map( ( tag ) => tag.trim() );
}

setFilters( { ...filters, tags: values } );
updatedFilters = { ...filters, tags: values };
} else {
setFilters( { ...filters, [ filterType ]: newValue } );
updatedFilters = { ...filters, [ filterType ]: newValue };
}

setFilters( updatedFilters );
fetchPosts( period, metric, updatedFilters, FETCH_RETRIES );
};

// No filter data could be retrieved. Prevent the component from rendering.
Expand Down Expand Up @@ -329,7 +369,7 @@ export const RelatedPostsPanel = (): React.JSX.Element => {
{ __( 'Loading…', 'wp-parsely' ) }
</div>
) }
{ ! loading && ! error && posts.length === 0 && (
{ ! firstRun && ! loading && ! error && posts.length === 0 && (
<div className="related-posts-empty">
{ __( 'No related posts found.', 'wp-parsely' ) }
</div>
Expand Down
153 changes: 153 additions & 0 deletions src/content-helper/editor-sidebar/related-posts/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* WordPress dependencies
*/
import { createReduxStore, register } from '@wordpress/data';

/**
* Internal dependencies
*/
import { PostFilters } from '../../common/utils/constants';
import { PostData } from '../../common/utils/post';

/**
* Represents the state for the Related Posts sidebar component.
*
* @since 3.18.0
*/
interface PostsState {
isFirstRun: boolean;
isLoading: boolean;
filters: PostFilters;
posts: PostData[];
}

interface SetFirstRunAction {
type: 'SET_FIRST_RUN';
isFirstRun: boolean;
}

interface SetLoadingAction {
type: 'SET_LOADING';
isLoading: boolean;
}

interface SetPostsAction {
type: 'SET_POSTS';
posts: PostData[];
}

interface SetFilterParamsAction {
type: 'SET_FILTERS';
filters: PostFilters;
}

interface ResetAction {
type: 'RESET';
}

type ActionTypes = SetFirstRunAction | SetLoadingAction | SetPostsAction | SetFilterParamsAction | ResetAction;

const defaultState: PostsState = {
isFirstRun: true,
isLoading: false,
filters: {
author: '',
section: '',
tags: [],
},
posts: [],
};

/**
* Redux store for managing filter and posts state for the Related Posts sidebar component.
*
* @since 3.18.0
*/
export const RelatedPostsStore = createReduxStore( 'wp-parsely/related-posts', {
initialState: defaultState,
reducer( state: PostsState = defaultState, action: ActionTypes ): PostsState {
switch ( action.type ) {
case 'SET_FIRST_RUN':
return {
...state,
isFirstRun: action.isFirstRun,
};
case 'SET_LOADING':
return {
...state,
isLoading: action.isLoading,
};
case 'SET_POSTS':
return {
...state,
posts: action.posts,
};
case 'SET_FILTERS':
return {
...state,
filters: action.filters,
};
case 'RESET':
return defaultState;
default:
return state;
}
},
actions: {
setFirstRun( isFirstRun: boolean ): SetFirstRunAction {
return {
type: 'SET_FIRST_RUN',
isFirstRun,
};
},
setLoading( isLoading: boolean ): SetLoadingAction {
return {
type: 'SET_LOADING',
isLoading,
};
},
setPosts( posts: PostData[] ): SetPostsAction {
return {
type: 'SET_POSTS',
posts,
};
},
setFilters( filters: PostFilters ): SetFilterParamsAction {
return {
type: 'SET_FILTERS',
filters,
};
},
/**
* Resets the state to the default state. Useful for testing.
*
* @since 3.18.0
*
* @return {ResetAction} Action object for resetting the state.
*/
reset(): ResetAction {
return {
type: 'RESET',
};
},
},
selectors: {
getState( state: PostsState ): PostsState {
return state;
},
isFirstRun( state: PostsState ): boolean {
return state.isFirstRun;
},
isLoading( state: PostsState ): boolean {
return state.isLoading;
},
getPosts( state: PostsState ): PostData[] {
return state.posts;
},
getFilters( state: PostsState ) {
return state.filters;
},
},
} );

register( RelatedPostsStore );
13 changes: 13 additions & 0 deletions tests/js/content-helper/structure.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
waitFor,
} from '@testing-library/react';

/**
* WordPress dependencies
*/
import { dispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
Expand All @@ -26,6 +31,9 @@ import {
RELATED_POSTS_DEFAULT_LIMIT,
RelatedPostsProvider,
} from '../../../src/content-helper/editor-sidebar/related-posts/provider';
import {
RelatedPostsStore,
} from '../../../src/content-helper/editor-sidebar/related-posts/store';

// Avoid "ReferenceError: ResizeObserver is not defined" error.
window.ResizeObserver =
Expand Down Expand Up @@ -119,6 +127,11 @@ jest.mock( '../../../src/content-helper/editor-sidebar/related-posts/hooks', ()
const relatedPostsPanel = <RelatedPostsPanel />;

describe( 'PCH Editor Sidebar Related Post panel', () => {
beforeEach( () => {
// Reset <RelatedPostsPanel /> to its initial state for each test.
dispatch( RelatedPostsStore ).reset();
} );

afterEach( () => {
jest.clearAllMocks();
setMockPostData( [ 'admin' ], [], [] );
Expand Down
Loading