PluginProbe
Parse.ly / 3.24.2
Parse.ly v3.24.2
3.24.3 3.24.2 3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 All 107 releases
wp-parsely / src / content-helper / dashboard-page / components / posts-table / component.tsx

component.tsx in Parse.ly 3.24.2, at src/content-helper/dashboard-page/components/posts-table/component.tsx

355 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress dependencies
3 */
4 import {
5 Button,
6 __experimentalNumberControl as NumberControl,
7 Spinner,
8 } from '@wordpress/components';
9 import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
10 import { __ } from '@wordpress/i18n';
11 import {
12 chevronLeft,
13 chevronRight,
14 } from '@wordpress/icons';
15
16 /**
17 * Internal dependencies
18 */
19 import {
20 ContentHelperError, ContentHelperErrorCode,
21 } from '../../../common/content-helper-error';
22 import { HydratedPost, QueryParams } from '../../../common/providers/base-wordpress-provider';
23 import { PostStats, StatsProvider } from '../../../common/providers/stats-provider';
24 import { DashboardProvider } from '../../provider';
25 import { SinglePostRow } from './components/single-post-row';
26
27 /**
28 * TablePagination component.
29 *
30 * Represents the pagination controls for the PostsTable.
31 *
32 * @since 3.19.0
33 *
34 * @param {Object} props The component's props.
35 * @param {boolean} props.isLoading Whether the posts are loading.
36 * @param {number} props.currentPage The current page.
37 * @param {Function} props.setCurrentPage The function to set the current page.
38 * @param {number} props.totalPages The total number of pages.
39 * @param {Function} props.onPrevious The function to handle the previous button click.
40 * @param {Function} props.onNext The function to handle the next button click.
41 */
42 const TablePagination = ( {
43 isLoading,
44 currentPage,
45 setCurrentPage,
46 totalPages,
47 onPrevious,
48 onNext,
49 }: {
50 isLoading: boolean;
51 currentPage: number;
52 setCurrentPage: ( value: number ) => void;
53 totalPages: number;
54 onPrevious: () => void;
55 onNext: () => void;
56 } ): React.JSX.Element => {
57 return (
58 <div className="posts-table-pagination-controls">
59 { isLoading && <Spinner /> }
60 <div className="page-selector">
61 <span>{ __( 'Page', 'wp-parsely' ) }</span>
62 <NumberControl
63 disabled={ isLoading }
64 value={ currentPage }
65 onChange={ ( value ) => {
66 let selectedPage = parseInt( value ?? '1', 10 );
67 if ( selectedPage > totalPages ) {
68 selectedPage = totalPages;
69 } else if ( selectedPage < 1 ) {
70 selectedPage = 1;
71 }
72
73 setCurrentPage( selectedPage );
74 } }
75 min={ 1 }
76 max={ totalPages }
77 dragDirection="e"
78 />
79 <span>{ __( 'of', 'wp-parsely' ) } { totalPages }</span>
80 </div>
81 <div className="page-navigation">
82 <Button icon={ chevronLeft } onClick={ onPrevious } disabled={ currentPage === 1 || isLoading } />
83 <Button icon={ chevronRight } onClick={ onNext } disabled={ currentPage >= totalPages || isLoading } />
84 </div>
85 </div>
86 );
87 };
88
89 /**
90 * Type definition for the PostsTable component.
91 *
92 * @since 3.19.0
93 */
94 type PostsTableType = {
95 query?: QueryParams;
96 currentPage: number;
97 setCurrentPage: ( value: number | ( ( prev: number ) => number ) ) => void;
98 hideHeader?: boolean;
99 hidePagination?: boolean;
100 hideLoading?: boolean;
101 hideStats?: boolean;
102 hideActions?: boolean;
103 compact?: boolean;
104 noResultsMessage?: React.ReactNode;
105 className?: string;
106 onPostClick?: ( post: HydratedPost ) => void;
107 };
108
109 /**
110 * PostsTable component.
111 *
112 * Represents a table of posts, that support custom queries and pagination.
113 *
114 * @since 3.19.0
115 *
116 * @param {PostsTableType} props The component's props.
117 */
118 export const PostsTable = ( {
119 query = {},
120 currentPage,
121 setCurrentPage,
122 hideHeader = false,
123 hidePagination = false,
124 hideLoading = false,
125 hideStats = false,
126 hideActions = false,
127 compact = false,
128 noResultsMessage = __( 'No posts found.', 'wp-parsely' ),
129 className,
130 onPostClick,
131 }: PostsTableType ): React.JSX.Element => {
132 // TODO: Add a global state to store the posts for faster loading.
133 const [ posts, setPosts ] = useState<HydratedPost[]>( [] );
134 const [ stats, setStats ] = useState<PostStats[]>( [] );
135
136 const [ totalPages, setTotalPages ] = useState<number>( 1 );
137 const [ itemsPerPage ] = useState<number>( query.per_page ?? 10 );
138
139 const [ isLoading, setIsLoading ] = useState<boolean>( true );
140 const [ isLoadingStats, setIsLoadingStats ] = useState<boolean>( true );
141
142 const [ error, setError ] = useState<ContentHelperError>();
143
144 const didFirstSearch = useRef( false );
145
146 /**
147 * Fetches posts from the API, using the query and pagination.
148 *
149 * @since 3.19.0
150 */
151 useEffect( () => {
152 const fetchPosts = async () => {
153 try {
154 const fetchedPosts = await DashboardProvider.getInstance().getPosts( {
155 context: 'embed',
156 ...query,
157 per_page: itemsPerPage,
158 page: currentPage,
159 } );
160
161 if ( ! hideStats ) {
162 // Fetch the stats for the posts.
163 const fetchedStats = await StatsProvider.getInstance().getStatsForPosts( fetchedPosts.data, {
164 limit: fetchedPosts.data.length,
165 period_start: '30d',
166 page: 1,
167 campaign_id: 'wp-parsely',
168 campaign_medium: 'smart-link',
169 } );
170
171 setStats( fetchedStats );
172 }
173
174 // Set the posts and total pages after fetching the stats.
175 setPosts( fetchedPosts.data );
176 setTotalPages( fetchedPosts.total_pages );
177 didFirstSearch.current = true;
178 } catch ( fetchError: unknown ) {
179 console.error( fetchError ); // eslint-disable-line no-console
180
181 if ( fetchError instanceof ContentHelperError ) {
182 setError( fetchError );
183 }
184
185 if ( fetchError instanceof Error ) {
186 setError( new ContentHelperError(
187 fetchError.message,
188 ContentHelperErrorCode.UnknownError )
189 );
190 }
191 } finally {
192 setIsLoading( false );
193 setIsLoadingStats( false );
194 }
195 };
196
197 setIsLoading( true );
198 setIsLoadingStats( true );
199
200 fetchPosts();
201 }, [ currentPage, itemsPerPage, query, hideStats ] );
202
203 /**
204 * Handles when the initial stats loading fails.
205 *
206 * It tries to fetch the stats again for this URL, but instead try with the WordPress permalink.
207 *
208 * @since 3.19.0
209 *
210 * @param {HydratedPost} post The post to fetch the stats for.
211 */
212 const onErrorLoadingStats = useCallback( async ( post: HydratedPost ) => {
213 try {
214 // Try to fetch the stats again for this URL, but instead try with the WordPress permalink.
215 // The API will then try to convert the permalink to a canonical URL, and use
216 // both the permalink and the canonical URL to fetch the stats.
217 const fetchedStats = await StatsProvider.getInstance().getStatsForPosts( [ post ], {
218 limit: 1,
219 period_start: '30d',
220 page: 1,
221 campaign_id: 'wp-parsely',
222 campaign_medium: 'smart-link',
223 use_wp_permalink: true,
224 } );
225
226 setStats( ( prevStats ) => [ ...prevStats, fetchedStats[ 0 ] ] );
227 } catch ( fetchError ) {
228 console.error( fetchError ); // eslint-disable-line no-console
229 }
230 }, [] );
231
232 /**
233 * Handles the previous button click.
234 *
235 * @since 3.19.0
236 */
237 const handlePrevious = () => {
238 setCurrentPage( ( prev ) => Math.max( prev - 1, 1 ) );
239 };
240
241 /**
242 * Handles the next button click.
243 *
244 * @since 3.19.0
245 */
246 const handleNext = () => {
247 setCurrentPage( ( prev ) => prev + 1 );
248 };
249
250 /**
251 * Gets the stats for a specific post.
252 *
253 * @since 3.19.0
254 *
255 * @param {HydratedPost} post The post to get the stats for.
256 * @return {PostStats} The stats for the post.
257 */
258 const getStatsForPost = useCallback( ( post: HydratedPost ) => {
259 return stats.find( ( stat ) => stat.postId === post.id );
260 }, [ stats ] );
261
262 const tableClasses: string[] = [ 'parsely-table-container' ];
263 if ( className ) {
264 tableClasses.push( className );
265 }
266
267 // Hide the table if loading and hideLoading is true.
268 if ( isLoading && hideLoading && ! didFirstSearch.current ) {
269 return <></>;
270 }
271
272 // Show a loading spinner if the posts are still loading.
273 if ( isLoading && ! hideLoading && posts.length === 0 ) {
274 tableClasses.push( 'is-loading' );
275 return (
276 <div className={ tableClasses.join( ' ' ) }>
277 <Spinner />
278 </div>
279 );
280 }
281
282 if ( error && error.code !== ContentHelperErrorCode.ParselyApiReturnedNoData ) {
283 return (
284 <div className="parsely-table-container no-results">
285 { error.Message() }
286 </div>
287 );
288 }
289
290 // Show a "no results" message if there are no posts.
291 if ( posts.length === 0 ) {
292 return (
293 <div className="parsely-table-container no-results">
294 { noResultsMessage }
295 </div>
296 );
297 }
298
299 if ( hideHeader ) {
300 tableClasses.push( 'hide-header' );
301 }
302 if ( hidePagination ) {
303 tableClasses.push( 'hide-pagination' );
304 }
305 if ( compact ) {
306 tableClasses.push( 'compact' );
307 }
308
309 return (
310 <div className={ tableClasses.join( ' ' ) }>
311 <table className={ tableClasses.join( ' ' ) }>
312 { ! hideHeader && (
313 <thead>
314 <tr>
315 <th scope="col" className="post-info-header">{ __( 'POST', 'wp-parsely' ) }</th>
316 { ! compact && (
317 <th scope="col" className="views-header">
318 { __( 'VIEWS', 'wp-parsely' ) }
319 <span className="views-header-period">{ __( '(30 days)', 'wp-parsely' ) }</span>
320 </th>
321 ) }
322 </tr>
323 </thead>
324 ) }
325 <tbody>
326 { posts.map( ( post, index ) => (
327 <SinglePostRow
328 key={ post.id }
329 post={ post }
330 stats={ getStatsForPost( post ) }
331 isLoadingStats={ isLoadingStats }
332 index={ index }
333 onPostClick={ onPostClick }
334 compact={ compact }
335 showStats={ ! hideStats }
336 showActions={ ! hideActions }
337 onErrorLoadingStats={ onErrorLoadingStats }
338 />
339 ) ) }
340 </tbody>
341 </table>
342 { ! hidePagination && (
343 <TablePagination
344 isLoading={ isLoading }
345 currentPage={ currentPage }
346 setCurrentPage={ setCurrentPage }
347 totalPages={ totalPages }
348 onPrevious={ handlePrevious }
349 onNext={ handleNext }
350 />
351 ) }
352 </div>
353 );
354 };
355