Skip to content

Commit fccbd08

Browse files
GaryJonesclaude
andcommitted
fix: restore Authors autocomplete after react-select v5 migration
PR #714 migrated to React 18 and @wordpress/scripts, which upgraded react-select from v1 to v5. This introduced breaking API changes that broke the Authors field in the entry editor, preventing autocomplete functionality and causing 500 errors when creating entries with multiple authors. The react-select v5 upgrade changed several prop names and component APIs that required corresponding updates throughout the codebase. The Authors autocomplete was making API requests without authentication headers, resulting in 401 errors. Additionally, the PHP backend wasn't properly handling arrays of contributor IDs from the multi-select field, and RxJS operators needed updating to use the modern pipe syntax. Changes: - Migrate react-select props: multi→isMulti, valueKey/labelKey→getter functions, optionComponent→components, clearable→isClearable, cache→cacheOptions - Update AuthorSelectOption to handle v5's data prop instead of option, and selectOption callback instead of onSelect - Convert getUsers to return Promise for react-select-async-paginate compatibility instead of using callback pattern - Add X-WP-Nonce authentication headers to getAuthors and getHashtags API requests to fix 401 errors - Update PHP get_json_param to properly handle arrays using array_map for HTML entity decoding of contributor_ids - Fix RxJS operators to use pipe syntax (timeout, map) - Enable webpack source maps for debugging Fixes #700 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 473e10f commit fccbd08

File tree

12 files changed

+82
-32
lines changed

12 files changed

+82
-32
lines changed

assets/920.bundle.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/920.bundle.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/985.bundle.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/985.bundle.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/app.asset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime'), 'version' => 'ef3c3690771551fc2b10');
1+
<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime'), 'version' => 'b06081e9495dadca9d93');

assets/app.js

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/app.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

classes/class-wpcom-liveblog-rest-api.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,10 @@ public static function sanitize_numeric( $param, $request, $key ) {
660660
*/
661661
public static function get_json_param( $param, $json ) {
662662
if ( isset( $json[ $param ] ) ) {
663+
// Handle arrays (e.g., contributor_ids from multi-select)
664+
if ( is_array( $json[ $param ] ) ) {
665+
return array_map( 'html_entity_decode', $json[ $param ] );
666+
}
663667
return html_entity_decode( $json[ $param ] );
664668
}
665669
return false;

src/react/components/AuthorSelectOption.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,44 @@ import PropTypes from 'prop-types';
33

44
class AuthorSelectOption extends Component {
55
handleMouseDown(event) {
6-
const { onSelect, option } = this.props;
6+
const { onSelect, option, data, selectOption } = this.props;
7+
const item = data || option;
78
event.preventDefault();
89
event.stopPropagation();
9-
onSelect(option, event);
10+
// react-select v5 uses selectOption instead of onSelect
11+
if (selectOption) {
12+
selectOption(item);
13+
} else if (onSelect) {
14+
onSelect(item, event);
15+
}
1016
}
1117

1218
handleMouseEnter(event) {
13-
this.props.onFocus(this.props.option, event);
19+
const { onFocus, option, data } = this.props;
20+
const item = data || option;
21+
if (onFocus) onFocus(item, event);
1422
}
1523

1624
handleMouseMove(event) {
17-
const { isFocused, onFocus, option } = this.props;
25+
const { isFocused, onFocus, option, data } = this.props;
1826
if (isFocused) return;
19-
onFocus(option, event);
27+
const item = data || option;
28+
if (onFocus) onFocus(item, event);
2029
}
2130

2231
render() {
23-
const { className, option } = this.props;
32+
const { className, option, data } = this.props;
33+
// react-select v5 uses 'data' prop instead of 'option'
34+
const item = data || option || {};
2435
return (
2536
<div
2637
className={`${className} liveblog-popover-item`}
2738
onMouseDown={this.handleMouseDown.bind(this)}
2839
onMouseEnter={this.handleMouseEnter.bind(this)}
2940
onMouseMove={this.handleMouseMove.bind(this)}
3041
>
31-
{ option.avatar && <div dangerouslySetInnerHTML={{ __html: option.avatar }} /> }
32-
{option.name}
42+
{ item.avatar && <div dangerouslySetInnerHTML={{ __html: item.avatar }} /> }
43+
{item.name}
3344
</div>
3445
);
3546
}

src/react/containers/EditorContainer.js

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { connect } from 'react-redux';
88
import { AsyncPaginate as Async } from 'react-select-async-paginate';
99
import { html } from 'js-beautify';
1010
import { debounce } from 'lodash-es';
11+
import { timeout, map } from 'rxjs/operators';
1112

1213
import { EditorState, ContentState } from 'draft-js';
1314

@@ -60,7 +61,8 @@ class EditorContainer extends Component {
6061
rawText: html(convertToHTML(editorState.getCurrentContent())),
6162
});
6263

63-
this.getUsers = debounce(this.getUsers.bind(this), props.config.author_list_debounce_time);
64+
// Note: getUsers must return a Promise for react-select-async-paginate
65+
// Debouncing is handled by the library itself, so we don't debounce here
6466
}
6567

6668
setReadOnly(state) {
@@ -139,22 +141,38 @@ class EditorContainer extends Component {
139141
});
140142
}
141143

142-
getUsers(text, callback) {
144+
getUsers(text) {
143145
const { config } = this.props;
144-
getAuthors(text, config)
145-
.timeout(10000)
146-
.map(res => res.response)
147-
.subscribe(res => callback(null, {
148-
options: res,
149-
complete: false,
150-
}));
146+
147+
// Handle empty text - return empty options immediately
148+
if (!text || text.trim() === '') {
149+
return Promise.resolve({ options: [] });
150+
}
151+
152+
return new Promise((resolve) => {
153+
getAuthors(text, config)
154+
.pipe(
155+
timeout(10000),
156+
map(res => res.response),
157+
)
158+
.subscribe({
159+
next: res => resolve({ options: res || [] }),
160+
error: err => {
161+
// Fail gracefully with empty options on error (e.g., 401)
162+
console.warn('Authors API error:', err);
163+
resolve({ options: [] });
164+
},
165+
});
166+
});
151167
}
152168

153169
getAuthors(text) {
154170
const { config } = this.props;
155171
getAuthors(text, config)
156-
.timeout(10000)
157-
.map(res => res.response)
172+
.pipe(
173+
timeout(10000),
174+
map(res => res.response),
175+
)
158176
.subscribe(res => this.setState({
159177
suggestions: res.map(author => author),
160178
}));
@@ -163,8 +181,10 @@ class EditorContainer extends Component {
163181
getHashtags(text) {
164182
const { config } = this.props;
165183
getHashtags(text, config)
166-
.timeout(10000)
167-
.map(res => res.response)
184+
.pipe(
185+
timeout(10000),
186+
map(res => res.response),
187+
)
168188
.subscribe(res => this.setState({
169189
suggestions: res.map(hashtag => hashtag),
170190
}));
@@ -299,15 +319,15 @@ class EditorContainer extends Component {
299319
}
300320
<h2 className="liveblog-editor-subTitle">Authors:</h2>
301321
<Async
302-
multi={true}
322+
isMulti={true}
303323
value={authors}
304-
valueKey="key"
305-
labelKey="name"
324+
getOptionValue={(option) => option.key}
325+
getOptionLabel={(option) => option.name}
306326
onChange={this.onSelectAuthorChange.bind(this)}
307-
optionComponent={AuthorSelectOption}
308-
loadOptions={this.getUsers}
309-
clearable={false}
310-
cache={false}
327+
components={{ Option: AuthorSelectOption }}
328+
loadOptions={this.getUsers.bind(this)}
329+
isClearable={false}
330+
cacheOptions={false}
311331
/>
312332
<button className="liveblog-btn liveblog-publish-btn" onClick={this.publish.bind(this)}>
313333
{isEditing ? 'Publish Update' : 'Publish New Entry'}

0 commit comments

Comments
 (0)