-
Notifications
You must be signed in to change notification settings - Fork 907
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create frontend providers and widget factory #22008
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed and moved into the |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/** | ||
* @type {import("../index").ContentType} ContentType | ||
* @type {import("../index").Features} Features | ||
* @type {import("../index").Endpoints} Endpoints | ||
* @type {import("../index").Links} Links | ||
* @type {import("../index").TopPageData} TopPageData | ||
*/ | ||
|
||
/** | ||
* Controls the data. | ||
*/ | ||
export class DataProvider { | ||
#contentTypes; | ||
#userName; | ||
#features; | ||
#endpoints; | ||
#headers; | ||
#links; | ||
|
||
/** | ||
* @param {ContentType[]} contentTypes The content types. | ||
* @param {string} userName The user name. | ||
* @param {Features} features Whether features are enabled. | ||
* @param {Endpoints} endpoints The endpoints. | ||
* @param {Object<string,string>} headers The headers for the WP requests. | ||
* @param {Links} links The links. | ||
*/ | ||
constructor( { contentTypes, userName, features, endpoints, headers, links } ) { | ||
this.#contentTypes = contentTypes; | ||
this.#userName = userName; | ||
this.#features = features; | ||
this.#endpoints = endpoints; | ||
this.#headers = headers; | ||
this.#links = links; | ||
} | ||
|
||
/** | ||
* @returns {Promise<ContentType[]>} The content types. | ||
*/ | ||
getContentTypes() { | ||
return this.#contentTypes; | ||
} | ||
|
||
/** | ||
* @returns {Promise<string>} The user name. | ||
*/ | ||
getUserName() { | ||
return this.#userName; | ||
} | ||
|
||
/** | ||
* @param {string} feature The feature to check. | ||
* @returns {Promise<boolean>} Whether the feature is enabled. | ||
*/ | ||
hasFeature( feature ) { | ||
return this.#features?.[ feature ] === true; | ||
} | ||
|
||
/** | ||
* @param {string} id The identifier. | ||
* @returns {Promise<?string>} The endpoint, if found. | ||
*/ | ||
getEndpoint( id ) { | ||
return this.#endpoints?.[ id ]; | ||
} | ||
|
||
/** | ||
* @returns {Promise<Object<string,string>>} The headers for making requests to the endpoints. | ||
*/ | ||
getHeaders() { | ||
return this.#headers; | ||
} | ||
|
||
/** | ||
* @param {string} id The identifier. | ||
* @returns {Promise<?string>} The link, if found. | ||
*/ | ||
getLink( id ) { | ||
return this.#links?.[ id ]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { defaultsDeep } from "lodash"; | ||
import { fetchJson } from "../fetch/fetch-json"; | ||
|
||
/** | ||
* Provides the mechanism to fetch data from a remote source. | ||
*/ | ||
export class RemoteDataProvider { | ||
#options; | ||
|
||
/** | ||
* @param {RequestInit} options The fetch options. | ||
* @throws {TypeError} If the baseUrl is invalid. | ||
*/ | ||
constructor( options ) { | ||
this.#options = options; | ||
} | ||
|
||
/** | ||
* @param {string|URL} endpoint The endpoint. | ||
* @param {Object<string,string>} [params] The query parameters. | ||
* @throws {TypeError} If the URL is invalid. | ||
* @link https://developer.mozilla.org/en-US/docs/Web/API/URL | ||
* @returns {URL} The URL. | ||
*/ | ||
getUrl( endpoint, params ) { | ||
const url = new URL( endpoint ); | ||
|
||
for ( const [ key, value ] of Object.entries( params ) ) { | ||
url.searchParams.append( key, value ); | ||
} | ||
|
||
return url; | ||
} | ||
|
||
/** | ||
* @param {string|URL} endpoint The endpoint. | ||
* @param {Object<string,string>} [params] The query parameters. | ||
* @param {RequestInit} [options] The request options. | ||
* @returns {Promise<any|Error>} The promise of a result, or an error. | ||
*/ | ||
async fetchJson( endpoint, params, options ) { | ||
return fetchJson( | ||
this.getUrl( endpoint, params ), | ||
defaultsDeep( options, this.#options, { headers: { "Content-Type": "application/json" } } ) | ||
); | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like a friendly hook for more data/error/isPending widgets. However, I'm not sure how caching/debouncing should be incorperated yet. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { createSlice } from "@reduxjs/toolkit"; | ||
import { useEffect, useReducer, useRef } from "@wordpress/element"; | ||
|
||
const slice = createSlice( { | ||
name: "data", | ||
initialState: { | ||
data: undefined, // eslint-disable-line no-undefined | ||
error: undefined, // eslint-disable-line no-undefined | ||
isPending: true, | ||
}, | ||
reducers: { | ||
setData( state, action ) { | ||
state.data = action.payload; | ||
state.error = undefined; // eslint-disable-line no-undefined | ||
state.isPending = false; | ||
}, | ||
setError( state, action ) { | ||
state.error = action.payload; | ||
state.isPending = false; | ||
}, | ||
setIsPending( state, action ) { | ||
state.isPending = Boolean( action.payload ); | ||
}, | ||
}, | ||
} ); | ||
|
||
export const useAsyncData = ( getter ) => { | ||
const [ state, dispatch ] = useReducer( slice.reducer, {}, slice.getInitialState ); | ||
/** @type {MutableRefObject<AbortController>} */ | ||
const controller = useRef(); | ||
|
||
useEffect( () => { | ||
// Abort any ongoing request first. | ||
controller.current?.abort(); | ||
controller.current = new AbortController(); | ||
|
||
dispatch( slice.actions.setIsPending( true ) ); | ||
getter( { signal: controller.current?.signal } ) | ||
.then( ( response ) => dispatch( slice.actions.setData( response ) ) ) | ||
.catch( ( e ) => { | ||
// Ignore abort errors, because they are expected and not to be reflected in the UI. | ||
if ( e?.name !== "AbortError" ) { | ||
dispatch( slice.actions.setError( e ) ); | ||
} | ||
} ); | ||
|
||
return () => controller.current?.abort(); | ||
}, [ getter ] ); | ||
|
||
return state; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* eslint-disable complexity */ | ||
import { ScoreWidget } from "../widgets/score-widget"; | ||
import { TopPagesWidget } from "../widgets/top-pages-widget"; | ||
|
||
/** | ||
* @type {import("../index").WidgetType} WidgetType | ||
* @type {import("../index").WidgetTypeInfo} WidgetTypeInfo | ||
*/ | ||
|
||
/** | ||
* Controls how to create a widget. | ||
*/ | ||
export class WidgetFactory { | ||
#dataProvider; | ||
#remoteDataProvider; | ||
|
||
/** | ||
* @param {import("./data-provider").DataProvider} dataProvider | ||
* @param {import("./remote-data-provider").RemoteDataProvider} remoteDataProvider | ||
*/ | ||
constructor( dataProvider, remoteDataProvider ) { | ||
this.#dataProvider = dataProvider; | ||
this.#remoteDataProvider = remoteDataProvider; | ||
} | ||
|
||
/** | ||
* @returns {WidgetTypeInfo[]} | ||
*/ | ||
static get widgetTypes() { | ||
return [ | ||
{ type: "seoScores" }, | ||
{ type: "readabilityScores" }, | ||
{ type: "topPages" }, | ||
]; | ||
} | ||
Comment on lines
+26
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe remove? As it is not needed yet, if ever |
||
|
||
/** | ||
* @param {WidgetInstance} widget The widget to create. | ||
* @param {function} onRemove The remove handler. | ||
* @returns {JSX.Element|null} The widget or null. | ||
*/ | ||
// eslint-disable-next-line no-unused-vars | ||
createWidget( widget, onRemove ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Probably nice to have some RemoveWidget base that handles this in the future? |
||
switch ( widget.type ) { | ||
case "seoScores": | ||
if ( ! this.#dataProvider.hasFeature( "indexables" ) && this.#dataProvider.hasFeature( "seoAnalysis" ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this move into the Widget? And is it worth an Widget layer if more need this? |
||
return null; | ||
} | ||
return <ScoreWidget key={ widget.id } analysisType="seo" dataProvider={ this.#dataProvider } />; | ||
case "readabilityScores": | ||
if ( ! this.#dataProvider.hasFeature( "indexables" ) && this.#dataProvider.hasFeature( "readabilityAnalysis" ) ) { | ||
return null; | ||
} | ||
return <ScoreWidget key={ widget.id } analysisType="readability" dataProvider={ this.#dataProvider } />; | ||
case "topPages": | ||
return <TopPagesWidget key={ widget.id } dataProvider={ this.#dataProvider } remoteDataProvider={ this.#remoteDataProvider } />; | ||
default: | ||
return null; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a widget too?