Skip to content

Commit b97d1c8

Browse files
authored
Merge pull request #3308 from 10up/feature/3296
Handle expired or missing tokens for orders autosuggest
2 parents e1f8982 + 5350509 commit b97d1c8

File tree

9 files changed

+240
-105
lines changed

9 files changed

+240
-105
lines changed

‎assets/js/api-search/index.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const Context = createContext();
4040
* @param {string} props.requestIdBase Base of Requests IDs.
4141
* @param {WPElement} props.children Component children.
4242
* @param {string} props.paramPrefix Prefix used to set and parse URL parameters.
43+
* @param {Function} props.onAuthError Function to run when request authentication fails.
4344
* @returns {WPElement} Component.
4445
*/
4546
export const ApiSearchProvider = ({
@@ -50,6 +51,7 @@ export const ApiSearchProvider = ({
5051
argsSchema,
5152
children,
5253
paramPrefix,
54+
onAuthError,
5355
}) => {
5456
/**
5557
* Any default args from the URL.
@@ -84,7 +86,13 @@ export const ApiSearchProvider = ({
8486
/**
8587
* Set up fetch method.
8688
*/
87-
const fetchResults = useFetchResults(apiHost, apiEndpoint, authorization, requestIdBase);
89+
const fetchResults = useFetchResults(
90+
apiHost,
91+
apiEndpoint,
92+
authorization,
93+
onAuthError,
94+
requestIdBase,
95+
);
8896

8997
/**
9098
* Set up the reducer.

‎assets/js/api-search/src/hooks.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ import { generateRequestId } from '../../utils/helpers';
1414
* @param {string} apiHost API host.
1515
* @param {string} apiEndpoint API endpoint.
1616
* @param {string} Authorization Authorization header.
17+
* @param {Function} onAuthError Function to run when request authentication fails.
1718
* @param {string} requestIdBase Base of Request IDs.
1819
* @returns {Function} Function for retrieving search results.
1920
*/
20-
export const useFetchResults = (apiHost, apiEndpoint, Authorization, requestIdBase = '') => {
21+
export const useFetchResults = (
22+
apiHost,
23+
apiEndpoint,
24+
Authorization,
25+
onAuthError,
26+
requestIdBase = '',
27+
) => {
2128
const abort = useRef(new AbortController());
2229
const request = useRef(null);
30+
const onAuthErrorRef = useRef(onAuthError);
2331

2432
/**
2533
* Get new search results from the API.
@@ -48,6 +56,10 @@ export const useFetchResults = (apiHost, apiEndpoint, Authorization, requestIdBa
4856
headers,
4957
})
5058
.then((response) => {
59+
if (onAuthErrorRef.current && !response.ok) {
60+
onAuthErrorRef.current();
61+
}
62+
5163
return response.json();
5264
})
5365
.catch((error) => {

‎assets/js/woocommerce/admin/orders/app/components/listbox.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export default ({ children, id, input, label, onSelect }) => {
119119
}
120120

121121
event.preventDefault();
122-
onSelect(selectedIndexRef.current);
122+
123+
onSelect(selectedIndexRef.current, event.metaKey);
123124

124125
break;
125126
default:
@@ -153,6 +154,7 @@ export default ({ children, id, input, label, onSelect }) => {
153154
input.setAttribute('aria-autocomplete', 'list');
154155
input.setAttribute('aria-haspopup', true);
155156
input.setAttribute('aria-owns', id);
157+
input.setAttribute('autocomplete', 'off');
156158
input.setAttribute('role', 'combobox');
157159
input.addEventListener('focus', onFocus);
158160
input.addEventListener('keydown', onKeyDown);
@@ -162,6 +164,7 @@ export default ({ children, id, input, label, onSelect }) => {
162164
input.removeAttribute('aria-autocomplete');
163165
input.removeAttribute('aria-haspopup');
164166
input.removeAttribute('aria-owns');
167+
input.removeAttribute('autocomplete');
165168
input.removeAttribute('role');
166169
input.removeEventListener('focus', onFocus);
167170
input.removeEventListener('keydown', onKeyDown);
@@ -191,13 +194,12 @@ export default ({ children, id, input, label, onSelect }) => {
191194
aria-selected={selectedIndexRef.current === index}
192195
className="ep-listbox__option"
193196
key={child.props.id}
194-
onClick={() => onSelect(index)}
197+
onClick={(event) => onSelect(index, event.metaKey)}
195198
onMouseEnter={() => {
196199
setSelectedIndex(index);
197200
selectedIndexRef.current = index;
198201
}}
199202
role="option"
200-
tabIndex="-1"
201203
>
202204
{child}
203205
</li>

‎assets/js/woocommerce/admin/orders/app/components/shop-order.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* WordPress dependencies.
33
*/
4-
import { date, dateI18n } from '@wordpress/date';
4+
import { dateI18n } from '@wordpress/date';
55
import { WPElement } from '@wordpress/element';
66
import { _n, sprintf } from '@wordpress/i18n';
77

@@ -32,9 +32,9 @@ export default ({
3232
orderNumber,
3333
orderStatus,
3434
}) => {
35-
const formattedDate = dateI18n(dateFormat, orderDate, 'GMT');
36-
const formattedDateTime = date('c', orderDate, 'GMT');
37-
const formattedTime = dateI18n(timeFormat, orderDate, 'GMT');
35+
const formattedDate = dateI18n(dateFormat, orderDate);
36+
const formattedDateTime = dateI18n('c', orderDate);
37+
const formattedTime = dateI18n(timeFormat, orderDate);
3838
const statusClass = `status-${orderStatus.substring(3)}`;
3939
const statusLabel = statusLabels[orderStatus];
4040

‎assets/js/woocommerce/admin/orders/app/index.js

+14-10
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export default ({ adminUrl, input }) => {
3333
* @param {number} index Selected option index.
3434
*/
3535
const onSelect = useCallback(
36-
(index) => {
36+
(index, isMetaKey) => {
3737
const { post_id } = searchResultsRef.current[index]._source;
3838

39-
window.location = `${adminUrl}?post=${post_id}&action=edit`;
39+
window.open(`${adminUrl}?post=${post_id}&action=edit`, isMetaKey ? '_blank' : '_self');
4040
},
4141
[adminUrl],
4242
);
@@ -45,11 +45,7 @@ export default ({ adminUrl, input }) => {
4545
* Dis the change, with debouncing.
4646
*/
4747
const disInput = useDebounce((value) => {
48-
if (value) {
49-
searchFor(value);
50-
} else {
51-
clearResults();
52-
}
48+
searchFor(value);
5349
}, 300);
5450

5551
/**
@@ -59,9 +55,15 @@ export default ({ adminUrl, input }) => {
5955
*/
6056
const onInput = useCallback(
6157
(event) => {
62-
disInput(event.target.value);
58+
const { value } = event.target;
59+
60+
if (value) {
61+
disInput(event.target.value);
62+
} else {
63+
clearResults();
64+
}
6365
},
64-
[disInput],
66+
[clearResults, disInput],
6567
);
6668

6769
/**
@@ -102,6 +104,8 @@ export default ({ adminUrl, input }) => {
102104
post_status,
103105
} = option._source;
104106

107+
const orderDate = `${post_date_gmt.split(' ').join('T')}+00:00`;
108+
105109
return (
106110
<ShopOrder
107111
emailAddress={billing_email}
@@ -110,7 +114,7 @@ export default ({ adminUrl, input }) => {
110114
itemCount={items ? items.split('|').length : 0}
111115
key={post_id}
112116
lastName={billing_last_name}
113-
orderDate={post_date_gmt}
117+
orderDate={orderDate}
114118
orderNumber={post_id}
115119
orderStatus={post_status}
116120
/>

‎assets/js/woocommerce/admin/orders/config.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,22 @@ const {
55
adminUrl,
66
apiEndpoint,
77
apiHost,
8+
credentialsApiUrl,
9+
credentialsNonce,
810
argsSchema,
9-
authorization,
1011
dateFormat,
1112
statusLabels,
1213
timeFormat,
1314
requestIdBase,
14-
} = window.epWooCommerceAdminOrders;
15+
} = window.epWooCommerceOrderSearch;
1516

1617
export {
1718
adminUrl,
1819
apiEndpoint,
1920
apiHost,
2021
argsSchema,
21-
authorization,
22+
credentialsApiUrl,
23+
credentialsNonce,
2224
dateFormat,
2325
statusLabels,
2426
timeFormat,
+89-27
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
22
* WordPress dependencies.
33
*/
4-
import domReady from '@wordpress/dom-ready';
5-
import { render } from '@wordpress/element';
4+
import { render, useEffect, useRef, useState, WPElement } from '@wordpress/element';
65

76
/**
87
* Internal dependencies.
@@ -14,47 +13,110 @@ import {
1413
apiEndpoint,
1514
apiHost,
1615
argsSchema,
17-
authorization,
18-
dateFormat,
19-
statusLabels,
20-
timeFormat,
16+
credentialsApiUrl,
17+
credentialsNonce,
2118
requestIdBase,
2219
} from './config';
2320

21+
/**
22+
* Order search provider component.
23+
*
24+
* Bundles several provider components with authentication handling.
25+
*
26+
* @param {object} props Component props.
27+
* @param {WPElement} props.children Component children.
28+
* @returns {WPElement}
29+
*/
30+
const AuthenticatedApiSearchProvider = ({ children }) => {
31+
/**
32+
* State.
33+
*/
34+
const [credentials, setCredentials] = useState(null);
35+
36+
/**
37+
* Refs.
38+
*/
39+
const hasRefreshed = useRef(false);
40+
41+
/**
42+
* Refresh credentials on authentication errors.
43+
*
44+
* @returns {void}
45+
*/
46+
const onAuthError = () => {
47+
if (hasRefreshed.current) {
48+
setCredentials(null);
49+
return;
50+
}
51+
52+
fetch(credentialsApiUrl, {
53+
headers: { 'X-WP-Nonce': credentialsNonce },
54+
method: 'POST',
55+
})
56+
.then((response) => response.text())
57+
.then(setCredentials);
58+
59+
hasRefreshed.current = true;
60+
};
61+
62+
/**
63+
* Set credentials on initialization.
64+
*
65+
* @returns {void}
66+
*/
67+
const onInit = () => {
68+
fetch(credentialsApiUrl, {
69+
headers: { 'X-WP-Nonce': credentialsNonce },
70+
})
71+
.then((response) => response.text())
72+
.then(setCredentials);
73+
};
74+
75+
/**
76+
* Effects.
77+
*/
78+
useEffect(onInit, []);
79+
80+
/**
81+
* Render.
82+
*/
83+
return credentials ? (
84+
<ApiSearchProvider
85+
apiEndpoint={apiEndpoint}
86+
apiHost={apiHost}
87+
argsSchema={argsSchema}
88+
authorization={`Basic ${credentials}`}
89+
requestIdBase={requestIdBase}
90+
onAuthError={onAuthError}
91+
>
92+
{children}
93+
</ApiSearchProvider>
94+
) : null;
95+
};
96+
2497
/**
2598
* Initialize.
99+
*
100+
* @returns {void}
26101
*/
27-
const init = () => {
102+
const init = async () => {
28103
const form = document.getElementById('posts-filter');
29104
const input = form.s;
30105

31106
if (!input) {
32107
return;
33108
}
34109

35-
const app = document.createElement('div');
110+
const el = document.createElement('div');
36111

37-
input.parentElement.appendChild(app);
112+
input.parentElement.appendChild(el);
38113

39114
render(
40-
<ApiSearchProvider
41-
apiEndpoint={apiEndpoint}
42-
apiHost={apiHost}
43-
argsSchema={argsSchema}
44-
authorization={authorization}
45-
requestIdBase={requestIdBase}
46-
defaultIsOn
47-
>
48-
<App
49-
adminUrl={adminUrl}
50-
dateFormat={dateFormat}
51-
input={input}
52-
statusLabels={statusLabels}
53-
timeFormat={timeFormat}
54-
/>
55-
</ApiSearchProvider>,
56-
app,
115+
<AuthenticatedApiSearchProvider>
116+
<App adminUrl={adminUrl} input={input} />
117+
</AuthenticatedApiSearchProvider>,
118+
el,
57119
);
58120
};
59121

60-
domReady(init);
122+
init();

0 commit comments

Comments
 (0)