Skip to content

Commit d2d881b

Browse files
authored
Merge pull request #23269 from Yoast/1155-cache-get_recently_modified_posts-results
1155 cache get recently modified posts results
2 parents f57df8a + 032eb8f commit d2d881b

26 files changed

Lines changed: 1207 additions & 102 deletions

File tree

packages/js/src/ai-content-planner/hooks/use-fetch-content-outline.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import { removesLocaleVariantSuffixes } from "../../shared-admin/helpers";
1616
* @returns {(contentSuggestion: Suggestion) => void} Callback to trigger the content outline fetch.
1717
*/
1818
export const useFetchContentOutline = () => {
19-
const { endpoint, postType, contentLocale, editorApiValue, selectContentOutlineCache } = useSelect( ( select ) => {
19+
const { endpoint, postType, contentLocale, editorApiValue, recentContent, selectContentOutlineCache } = useSelect( ( select ) => {
2020
return {
2121
endpoint: select( CONTENT_PLANNER_STORE ).selectContentOutlineEndpoint(),
2222
postType: select( "yoast-seo/editor" ).getPostType(),
2323
contentLocale: select( "yoast-seo/editor" ).getContentLocale(),
2424
editorApiValue: select( "yoast-seo/editor" ).getEditorTypeApiValue(),
25+
recentContent: select( CONTENT_PLANNER_STORE ).selectRecentContent(),
2526
selectContentOutlineCache: select( CONTENT_PLANNER_STORE ).selectContentOutlineCache,
2627
};
2728
}, [] );
@@ -42,6 +43,7 @@ export const useFetchContentOutline = () => {
4243
postType,
4344
language,
4445
editor: editorApiValue,
46+
recentContent,
4547
suggestion: {
4648
...contentSuggestion,
4749
},
@@ -50,6 +52,7 @@ export const useFetchContentOutline = () => {
5052
postType,
5153
contentLocale,
5254
editorApiValue,
55+
recentContent,
5356
fetchContentOutline,
5457
restoreContentOutlineFromCache,
5558
selectContentOutlineCache,

packages/js/src/ai-content-planner/store/content-outline.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,16 @@ export const contentOutlineSelectors = {
107107
};
108108

109109
/**
110-
* @param {string} endpoint The endpoint to fetch the content outline from.
111-
* @param {string} postType The type of the post.
112-
* @param {string} language The language of the post.
113-
* @param {string} editor The editor instance.
114-
* @param {Suggestion} suggestion The suggestion object containing details for the content outline.
110+
* @param {string} endpoint The endpoint to fetch the content outline from.
111+
* @param {string} postType The type of the post.
112+
* @param {string} language The language of the post.
113+
* @param {string} editor The editor instance.
114+
* @param {Suggestion} suggestion The suggestion object containing details for the content outline.
115+
* @param {Object[]} recentContent The recent content from the suggestions response.
115116
* @returns {Object} Success or error action object.
116117
*/
117118
export function* fetchContentOutline( {
118-
endpoint, postType, language, editor, suggestion,
119+
endpoint, postType, language, editor, suggestion, recentContent,
119120
} ) {
120121
yield{ type: `${ FETCH_CONTENT_OUTLINE_ACTION_NAME }/${ ASYNC_ACTION_NAMES.request }`, payload: { suggestion } };
121122
try {
@@ -124,6 +125,7 @@ export function* fetchContentOutline( {
124125
postType,
125126
language,
126127
editor,
128+
recentContent,
127129
...suggestion,
128130
} };
129131
yield{ type: `${ FETCH_CONTENT_OUTLINE_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`, payload };
@@ -156,6 +158,8 @@ export const contentOutlineControls = {
156158
// eslint-disable-next-line camelcase
157159
meta_description: payload.meta_description,
158160
category: payload.category,
161+
// eslint-disable-next-line camelcase
162+
recent_content: payload.recentContent,
159163
},
160164
} ),
161165
};

packages/js/src/ai-content-planner/store/content-suggestions.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const slice = createSlice( {
1919
endpoint: "",
2020
status: ASYNC_ACTION_STATUS.idle,
2121
suggestions: [],
22+
recentContent: [],
2223
error: ERROR_DEFAULT,
2324
},
2425
reducers: {
@@ -43,11 +44,13 @@ const slice = createSlice( {
4344
builder.addCase( `${ FETCH_CONTENT_SUGGESTIONS_ACTION_NAME }/${ ASYNC_ACTION_NAMES.request }`, ( state ) => {
4445
state.status = ASYNC_ACTION_STATUS.loading;
4546
state.suggestions = [];
47+
state.recentContent = [];
4648
state.error = ERROR_DEFAULT;
4749
} );
4850
builder.addCase( `${ FETCH_CONTENT_SUGGESTIONS_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`, ( state, { payload } ) => {
4951
state.status = ASYNC_ACTION_STATUS.success;
50-
state.suggestions = payload;
52+
state.suggestions = payload.suggestions;
53+
state.recentContent = payload.recentContent;
5154
state.error = ERROR_DEFAULT;
5255
} );
5356
builder.addCase( `${ FETCH_CONTENT_SUGGESTIONS_ACTION_NAME }/${ ASYNC_ACTION_NAMES.error }`, ( state, { payload } ) => {
@@ -63,14 +66,15 @@ export const contentSuggestionsSelectors = {
6366
selectContentSuggestionsEndpoint: ( state ) => get( state, [ CONTENT_SUGGESTIONS_NAME, "endpoint" ], slice.getInitialState().endpoint ),
6467
selectSuggestionsStatus: ( state ) => get( state, [ CONTENT_SUGGESTIONS_NAME, "status" ], slice.getInitialState().status ),
6568
selectSuggestions: ( state ) => get( state, [ CONTENT_SUGGESTIONS_NAME, "suggestions" ], slice.getInitialState().suggestions ),
69+
selectRecentContent: ( state ) => get( state, [ CONTENT_SUGGESTIONS_NAME, "recentContent" ], slice.getInitialState().recentContent ),
6670
selectSuggestionsError: ( state ) => get( state, [ CONTENT_SUGGESTIONS_NAME, "error" ], slice.getInitialState().error ),
6771
};
6872
/**
6973
* Validates and transforms the API response into the expected suggestions shape.
7074
*
7175
* @param {Object} result The raw API response.
7276
*
73-
* @returns {Suggestion[]} The validated and transformed suggestions array.
77+
* @returns {{ suggestions: Suggestion[], recentContent: Object[] }} The validated and transformed response.
7478
*/
7579
const validateSuggestionsResponse = ( result ) => {
7680
const suggestions = get( result, "suggestions", null );
@@ -79,16 +83,19 @@ const validateSuggestionsResponse = ( result ) => {
7983
throw new Error( "Invalid suggestions response: expected an array of suggestions." );
8084
}
8185

82-
return suggestions.map( ( suggestion ) => ( {
83-
title: suggestion.title,
84-
intent: suggestion.intent,
85-
keyphrase: suggestion.keyphrase,
86-
// eslint-disable-next-line camelcase
87-
meta_description: suggestion.meta_description,
88-
category: suggestion.category,
89-
explanation: suggestion.explanation,
90-
id: `suggestion-${ suggestion.keyphrase }-${ suggestion.title }`,
91-
} ) );
86+
return {
87+
suggestions: suggestions.map( ( suggestion ) => ( {
88+
title: suggestion.title,
89+
intent: suggestion.intent,
90+
keyphrase: suggestion.keyphrase,
91+
// eslint-disable-next-line camelcase
92+
meta_description: suggestion.meta_description,
93+
category: suggestion.category,
94+
explanation: suggestion.explanation,
95+
id: `suggestion-${ suggestion.keyphrase }-${ suggestion.title }`,
96+
} ) ),
97+
recentContent: get( result, "recent_content", [] ),
98+
};
9299
};
93100
/**
94101
* Generator action to fetch content planner suggestions from the REST API.

packages/js/tests/ai-content-planner/hooks/use-fetch-content-outline.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const defaultStoreValues = {
2323
postType: "post",
2424
contentLocale: "en_US",
2525
editorApiValue: "classic",
26+
recentContent: [],
2627
selectContentOutlineCache: jest.fn( () => null ),
2728
};
2829

@@ -37,6 +38,7 @@ const setupUseSelect = ( overrides = {} ) => {
3738
if ( storeName === "yoast-seo/content-planner" ) {
3839
return {
3940
selectContentOutlineEndpoint: () => values.endpoint,
41+
selectRecentContent: () => values.recentContent,
4042
selectContentOutlineCache: values.selectContentOutlineCache,
4143
};
4244
}
@@ -140,6 +142,21 @@ describe( "useFetchContentOutline", () => {
140142

141143
expect( setFeatureModalStatus ).not.toHaveBeenCalled();
142144
} );
145+
146+
it( "passes recentContent from the store to fetchContentOutline", () => {
147+
const recentContent = [ { title: "My Post", description: "A description." } ];
148+
setupUseSelect( { recentContent } );
149+
150+
const { result } = renderHook( () => useFetchContentOutline() );
151+
152+
act( () => {
153+
result.current( mockSuggestion );
154+
} );
155+
156+
expect( fetchContentOutline ).toHaveBeenCalledWith( expect.objectContaining( {
157+
recentContent,
158+
} ) );
159+
} );
143160
} );
144161

145162
describe( "language locale transformation", () => {

packages/js/tests/ai-content-planner/store/outline.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const mockSuggestion = {
2929
};
3030
/* eslint-enable camelcase */
3131

32+
const mockRecentContent = [ { title: "My Post", description: "A description." } ];
33+
3234
describe( "content outline store", () => {
3335
describe( "getInitialContentOutlineState", () => {
3436
it( "should return the initial state", () => {
@@ -290,6 +292,7 @@ describe( "content outline store", () => {
290292
language: "en",
291293
editor: "gutenberg",
292294
suggestion: mockSuggestion,
295+
recentContent: mockRecentContent,
293296
};
294297

295298
it( "should yield a request action, a control action, a success action, then complete", () => {
@@ -312,6 +315,7 @@ describe( "content outline store", () => {
312315
postType: params.postType,
313316
language: params.language,
314317
editor: params.editor,
318+
recentContent: mockRecentContent,
315319
...mockSuggestion,
316320
},
317321
} );
@@ -382,6 +386,7 @@ describe( "content outline store", () => {
382386
// eslint-disable-next-line camelcase
383387
meta_description: mockSuggestion.meta_description,
384388
category: mockSuggestion.category,
389+
recentContent: mockRecentContent,
385390
};
386391
contentPlannerFetch.mockResolvedValue( { outline: [] } );
387392

@@ -402,6 +407,7 @@ describe( "content outline store", () => {
402407
keyphrase: payload.keyphrase,
403408
meta_description: payload.meta_description,
404409
category: payload.category,
410+
recent_content: mockRecentContent,
405411
} ),
406412
} )
407413
);

packages/js/tests/ai-content-planner/store/suggestions.test.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ describe( "suggestions store", () => {
7171
endpoint: "",
7272
status: ASYNC_ACTION_STATUS.idle,
7373
suggestions: [],
74+
recentContent: [],
7475
error: ERROR_DEFAULT,
7576
} );
7677
} );
@@ -82,6 +83,7 @@ describe( "suggestions store", () => {
8283
endpoint: "",
8384
status: ASYNC_ACTION_STATUS.success,
8485
suggestions: transformedSuggestions,
86+
recentContent: [ { title: "Old post" } ],
8587
error: ERROR_DEFAULT,
8688
};
8789

@@ -94,23 +96,26 @@ describe( "suggestions store", () => {
9496
endpoint: "",
9597
status: ASYNC_ACTION_STATUS.loading,
9698
suggestions: [],
99+
recentContent: [],
97100
error: ERROR_DEFAULT,
98101
} );
99102
} );
100103

101104
it( "should set suggestions and status to success on success", () => {
105+
const recentContent = [ { title: "My Post", description: "A description." } ];
102106
const result = contentSuggestionsReducer(
103107
getInitialContentSuggestionsState(),
104108
{
105109
type: `${ FETCH_CONTENT_SUGGESTIONS_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`,
106-
payload: transformedSuggestions,
110+
payload: { suggestions: transformedSuggestions, recentContent },
107111
}
108112
);
109113

110114
expect( result ).toEqual( {
111115
endpoint: "",
112116
status: ASYNC_ACTION_STATUS.success,
113117
suggestions: transformedSuggestions,
118+
recentContent,
114119
error: ERROR_DEFAULT,
115120
} );
116121
} );
@@ -134,6 +139,7 @@ describe( "suggestions store", () => {
134139
endpoint: "",
135140
status: ASYNC_ACTION_STATUS.error,
136141
suggestions: [],
142+
recentContent: [],
137143
error: {
138144
errorCode: 403,
139145
errorIdentifier: "forbidden",
@@ -289,6 +295,24 @@ describe( "suggestions store", () => {
289295
it( "should return the default error when state is missing", () => {
290296
expect( contentSuggestionsSelectors.selectSuggestionsError( {} ) ).toEqual( ERROR_DEFAULT );
291297
} );
298+
299+
it( "should return recent content from state", () => {
300+
const recentContent = [ { title: "My Post", description: "A description." } ];
301+
const state = {
302+
[ CONTENT_SUGGESTIONS_NAME ]: {
303+
status: ASYNC_ACTION_STATUS.success,
304+
suggestions: [],
305+
recentContent,
306+
error: ERROR_DEFAULT,
307+
},
308+
};
309+
310+
expect( contentSuggestionsSelectors.selectRecentContent( state ) ).toEqual( recentContent );
311+
} );
312+
313+
it( "should return an empty array for recent content when state is missing", () => {
314+
expect( contentSuggestionsSelectors.selectRecentContent( {} ) ).toEqual( [] );
315+
} );
292316
} );
293317

294318
describe( "fetchContentPlannerSuggestions", () => {
@@ -316,10 +340,11 @@ describe( "suggestions store", () => {
316340
} );
317341

318342
// Simulate the API response being returned from the control.
319-
const result = generator.next( { suggestions: mockApiSuggestions } );
343+
// eslint-disable-next-line camelcase
344+
const result = generator.next( { suggestions: mockApiSuggestions, recent_content: [] } );
320345
expect( result.value ).toEqual( {
321346
type: `${ FETCH_CONTENT_SUGGESTIONS_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`,
322-
payload: transformedSuggestions,
347+
payload: { suggestions: transformedSuggestions, recentContent: [] },
323348
} );
324349
expect( result.done ).toBe( true );
325350
} );

src/ai/content-planner/application/content-outline-command-handler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,9 @@ public function handle(
8282
Content_Outline_Command $command,
8383
bool $retry_on_unauthorized = true
8484
): Section_List {
85-
$recent_content = $this->recent_content_collector->collect( $command->get_post_type() );
85+
$recent_content = $command->get_recent_content();
8686
$about_page = $this->recent_content_collector->collect_about_page( $command->get_post_type() );
8787
$token = $this->token_manager->get_or_request_access_token( $command->get_user() );
88-
$recent_content = $recent_content->to_array();
8988

9089
$existing_posts = \array_map(
9190
static function ( $post ) {

src/ai/content-planner/application/content-outline-command.php

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,28 @@ class Content_Outline_Command {
8181
*/
8282
private $category;
8383

84+
/**
85+
* The recent content from the suggestions response.
86+
*
87+
* @var array<array<string, string|bool|array<string, int>>>
88+
*/
89+
private $recent_content;
90+
8491
/**
8592
* The constructor.
8693
*
87-
* @param WP_User $user The user.
88-
* @param string $post_type The post type.
89-
* @param string $language The language.
90-
* @param string $editor The editor.
91-
* @param string $title The title.
92-
* @param string $intent The intent.
93-
* @param string $explanation The explanation.
94-
* @param string $keyphrase The keyphrase.
95-
* @param string $meta_description The meta description.
96-
* @param string $category_name The category name.
97-
* @param int $category_id The category ID.
94+
* @param WP_User $user The user.
95+
* @param string $post_type The post type.
96+
* @param string $language The language.
97+
* @param string $editor The editor.
98+
* @param string $title The title.
99+
* @param string $intent The intent.
100+
* @param string $explanation The explanation.
101+
* @param string $keyphrase The keyphrase.
102+
* @param string $meta_description The meta description.
103+
* @param string $category_name The category name.
104+
* @param int $category_id The category ID.
105+
* @param array<array<string, string|bool|array<string, int>>> $recent_content The recent content from the suggestions response.
98106
*/
99107
public function __construct(
100108
WP_User $user,
@@ -107,7 +115,8 @@ public function __construct(
107115
string $keyphrase,
108116
string $meta_description,
109117
string $category_name,
110-
int $category_id
118+
int $category_id,
119+
array $recent_content
111120
) {
112121
$this->user = $user;
113122
$this->post_type = $post_type;
@@ -119,6 +128,7 @@ public function __construct(
119128
$this->keyphrase = $keyphrase;
120129
$this->meta_description = $meta_description;
121130
$this->category = new Category( $category_name, $category_id );
131+
$this->recent_content = $recent_content;
122132
}
123133

124134
/**
@@ -210,4 +220,13 @@ public function get_meta_description(): string {
210220
public function get_category(): Category {
211221
return $this->category;
212222
}
223+
224+
/**
225+
* Returns the recent content from the suggestions response.
226+
*
227+
* @return array<array<string, string|bool|array<string, int>>> The recent content.
228+
*/
229+
public function get_recent_content(): array {
230+
return $this->recent_content;
231+
}
213232
}

0 commit comments

Comments
 (0)