forked from woocommerce/google-listings-and-ads
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassets-loader.js
More file actions
210 lines (183 loc) · 6 KB
/
assets-loader.js
File metadata and controls
210 lines (183 loc) · 6 KB
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { useState, useRef } from '@wordpress/element';
import { Spinner } from '@woocommerce/components';
/**
* Internal dependencies
*/
import useDispatchCoreNotices from '.~/hooks/useDispatchCoreNotices';
import AppButton from '.~/components/app-button';
import SelectControl from '.~/wcdl/select-control';
import { API_NAMESPACE } from '.~/data/constants';
import './assets-loader.scss';
/**
* @typedef {import('.~/data/types.js').SuggestedAssets} SuggestedAssets
*/
function allowAllResults() {
// Make it result in `new RegExp('.', 'i')` to avoid any custom results in
// the mapFinalUrlsToOptions function being filtered out.
return '.';
}
function fetchFinalUrls( search ) {
const endPoint = `${ API_NAMESPACE }/assets/final-url/suggestions`;
const query = { search };
return apiFetch( { path: addQueryArgs( endPoint, query ) } );
}
function fetchSuggestedAssets( id, type ) {
const endPoint = `${ API_NAMESPACE }/assets/suggestions`;
const query = { id, type };
return apiFetch( { path: addQueryArgs( endPoint, query ) } );
}
function mapFinalUrlsToOptions( finalUrls, search ) {
const options = finalUrls.map( ( finalUrl ) => ( {
finalUrl,
key: `${ finalUrl.type }-${ finalUrl.id }`,
keywords: [ finalUrl.title ],
label: (
<>
<div className="gla-assets-loader__option-title">
{ finalUrl.title }
</div>
<div className="gla-assets-loader__option-url">
{ finalUrl.url }
</div>
</>
),
} ) );
// Querying with empty `search` means getting the suggestion pages by default.
// Prepend a label to indicate that the results are suggestions.
if ( search === '' && finalUrls.length ) {
options.unshift( {
key: 'disabled-option-suggestion',
label: __( 'SUGGESTIONS', 'google-listings-and-ads' ),
isDisabled: true,
} );
}
if ( finalUrls.length === 0 ) {
options.unshift( {
key: 'disabled-option-no-results',
label: __( 'No matching results', 'google-listings-and-ads' ),
keywords: [ search ],
isDisabled: true,
} );
}
return options;
}
/**
* Clicking on the "Scan for assets" button.
*
* @event gla_import_assets_by_final_url_button_click
* @property {string} type The type of the selected Final URL suggestion to be imported. Possible values: `post`, `term`, `homepage`.
*/
/**
* Renders the UI for querying pages, selecting a wanted page as the final URL,
* and then loading the suggested assets.
*
* @param {Object} props React props.
* @param {(suggestedAssets: SuggestedAssets) => void} props.onAssetsLoaded Callback function when the suggested assets are loaded.
*
* @fires gla_import_assets_by_final_url_button_click
*/
export default function AssetsLoader( { onAssetsLoaded } ) {
const cacheRef = useRef( {} );
const latestSearchRef = useRef();
// The selector allows only one option to be selected which is expected, and the array is used
// here to get the entire data of the selected option rather than getting its key only.
// Ref: https://github.com/woocommerce/woocommerce/blob/6.9.0/packages/js/components/src/select-control/index.js#L137-L141
const [ selectedOptions, setSelectedOptions ] = useState( [] );
const [ searching, setSearching ] = useState( false );
const [ fetching, setFetching ] = useState( false );
const { createNotice } = useDispatchCoreNotices();
// To have the searching state and keep the entered search value, this handler needs to
// be called immediately after keying values. Therefore, it also needs to implement the
// debounce here.
const debouncedHandleSearch = async ( prevOptions, rawSearch ) => {
if ( ! searching ) {
setSearching( true );
}
// Workaround to keep the entered search value.
if ( rawSearch !== selectedOptions[ 0 ]?.label ) {
setSelectedOptions( [ { label: rawSearch } ] );
}
// Workaround to debounce the calls.
const delay = new Promise( ( resolve ) => setTimeout( resolve, 300 ) );
latestSearchRef.current = delay;
await delay;
// Ensure only the latest call will be passed down.
if ( latestSearchRef.current !== delay ) {
return prevOptions;
}
const cache = cacheRef.current;
const search = rawSearch.trim().toLowerCase();
cache[ search ] ??= fetchFinalUrls( search ).then( ( finalUrls ) =>
mapFinalUrlsToOptions( finalUrls, search )
);
cache[ search ].finally( () => {
setSearching( false );
} );
return cache[ search ];
};
const handleChange = ( [ option ] ) => {
if ( option ) {
const selectedOption = { ...option, label: option.finalUrl.title };
setSelectedOptions( [ selectedOption ] );
} else {
setSelectedOptions( [] );
}
};
const handleClick = async () => {
const { finalUrl } = selectedOptions[ 0 ];
setFetching( true );
fetchSuggestedAssets( finalUrl.id, finalUrl.type )
.then( onAssetsLoaded )
.catch( () => {
setFetching( false );
createNotice(
'error',
__(
'Unable to load assets data from the selected page.',
'google-listings-and-ads'
)
);
} );
};
const { finalUrl } = selectedOptions[ 0 ] || {};
return (
<>
<SelectControl
className="gla-assets-loader"
label={
<>
{ __( 'Select final URL', 'google-listings-and-ads' ) }
{ searching && <Spinner /> }
</>
}
placeholder={ __( 'Search page', 'google-listings-and-ads' ) }
isSearchable
hideBeforeSearch
excludeSelectedOptions={ false }
disabled={ fetching }
options={ [] } // The actual options will be provided via the callback results of `onSearch`.
selected={ selectedOptions }
onSearch={ debouncedHandleSearch }
onChange={ handleChange }
getSearchExpression={ allowAllResults }
/>
<AppButton
isSecondary
text={
fetching ? '' : __( 'Select', 'google-listings-and-ads' )
}
eventName="gla_import_assets_by_final_url_button_click"
eventProps={ { type: finalUrl?.type } }
disabled={ ! finalUrl }
loading={ fetching }
onClick={ handleClick }
/>
</>
);
}