Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 12x 12x 12x 12x 12x 12x 9x 3x 3x 3x 12x 12x 12x 12x 12x 12x 12x 4x 8x 8x 8x 12x 12x 12x 12x | /**
* External dependencies
*/
import { usePostThumbnail, useStatsPost, useSiteHomeUrl } from '@jetpack-premium-analytics/data';
import { safeHttpUrl } from '@jetpack-premium-analytics/ui';
import { POST_URL_SEARCH_PARAM } from '@jetpack-premium-analytics/widgets-toolkit';
import { store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useCallback, useMemo } from '@wordpress/element';
import { useSearch } from '@wordpress/route';
/**
* Internal dependencies
*/
import type { EntitySummary } from '../adapter';
export type PostSummary = EntitySummary & {
/** Post type slug (e.g. `post`, `page`). */
type?: string;
/** Published date as a datetime string, when available. */
publishedDate?: string;
/** Featured image URL, when available. */
imageUrl?: string;
/** Public URL of the post/page, when available. */
url?: string;
};
/**
* Resolve the public URL a list surface carried into this route. Kept
* detailed: this is a security-relevant origin check, not a plain lookup.
*
* A `show_in_rest: false` post type has no core-data entity, so list surfaces
* pass the Stats-report URL through as a search param instead — honoured only
* for an http(s) URL matching the site's own origin (falling back to the
* dashboard's own origin when the site URL is unreadable), so a crafted link
* can't point the header out at another host.
*
* @return The carried URL when it is safe to use, otherwise `undefined`.
*/
function useCarriedPostUrl(): string | undefined {
const search = useSearch( { strict: false } ) as Record< string, unknown > | undefined;
const siteUrl = useSiteHomeUrl();
const carried = search?.[ POST_URL_SEARCH_PARAM ];
return useMemo( () => {
const candidate = safeHttpUrl( carried );
if ( ! candidate ) {
return undefined;
}
try {
const allowedOrigin = siteUrl ? new URL( siteUrl ).origin : window.location.origin;
return new URL( candidate ).origin === allowedOrigin ? candidate : undefined;
} catch {
return undefined;
}
}, [ carried, siteUrl ] );
}
/**
* Resolve the header summary for a single post/page. Title, type, and
* published date come from the Stats `post` payload; the featured image
* isn't in that payload, so it's read separately from core-data.
*
* @param postId - The post/page ID from the route.
* @return The resolved post summary.
*/
export function usePostSummary( postId: number ): PostSummary {
// The header only needs the post row, so scope the query to the `post` field
// instead of pulling the full stats payload.
const { data, isLoading, isPending, isPaused, isError, error, refetch } = useStatsPost( {
postId,
fields: [ 'post' ],
} );
const post = data?.post;
// A failed background refetch keeps the loaded post, so only a missing one is an error.
const hasFailed = isError && ! post;
const type = post?.post_type;
const imageUrl = usePostThumbnail( postId, type );
// Reads the same already-resolved post entity as the image (no extra
// request); its own selector avoids re-rendering on every store change.
const url = useSelect(
select => {
if ( ! type || ! Number.isInteger( postId ) || postId <= 0 ) {
return undefined;
}
const core = select( coreStore ) as unknown as {
getEntityRecord: (
kind: string,
name: string,
key: number,
query: Record< string, unknown >
) => unknown;
};
const entity = core.getEntityRecord( 'postType', type, postId, { context: 'view' } ) as
{ link?: string } | undefined;
return entity?.link;
},
[ postId, type ]
);
const carriedUrl = useCarriedPostUrl();
const retry = useCallback( () => {
void refetch();
}, [ refetch ] );
// GMT fallback gets an explicit `Z` suffix so downstream code doesn't
// mistake it for a site-timezone wall time.
const publishedDate =
post?.post_date ?? ( post?.post_date_gmt ? `${ post.post_date_gmt }Z` : undefined );
return {
title: post?.post_title,
type,
publishedDate,
allTimeStart: publishedDate,
imageUrl,
// The entity permalink is authoritative; the carried URL only covers the
// post types core data cannot resolve.
url: url ?? carriedUrl,
// React Query calls a first load paused offline or in a hidden tab not loading.
isLoading: isLoading || ( isPending && isPaused ),
isError: hasFailed,
error: hasFailed ? error : null,
// The Stats report answers for any id; a missing post reads as an error, as before.
isNotFound: false,
refetch: retry,
};
}
|