-
Notifications
You must be signed in to change notification settings - Fork 22
Add ability to share urls. #107
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
base: master
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 16.20.2 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ if ( wpcomConfig ) { | |
| id: 'WPCOM', | ||
| baseUrl: 'https://public-api.wordpress.com/oauth2', | ||
| userUrl: 'https://public-api.wordpress.com/rest/v1.1/me', | ||
| redirectUrl: wpcomConfig.redirectUrl || wpcomConfig.redirect_uri, | ||
| redirectUrl: window.location.href, | ||
|
Member
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. is this the proper change here? if someone uses the current location then it seems like it could lead to oauth failures, ass each oauth client application is setup for a specific redirect url. can you share some context on the change? |
||
| clientId: wpcomConfig.clientID || wpcomConfig.clientId || wpcomConfig.client_id, | ||
| scope: 'global', | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| { | ||
| "wordpress.com": { | ||
| "client_id": "12345", | ||
| "redirect_uri": "http://example.com/path/to/app" | ||
| "client_id": "12345" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,14 @@ | ||
| import { createStore, applyMiddleware } from 'redux'; | ||
| import thunk from 'redux-thunk'; | ||
|
|
||
| import router, { getStateFromUrl } from './router'; | ||
| import reducer from './reducer'; | ||
| import { boot } from './security/actions'; | ||
| import { loadInitialState, persistState } from '../lib/redux/cache'; | ||
|
|
||
| const store = createStore( | ||
| reducer, | ||
| loadInitialState( {}, reducer ), | ||
| applyMiddleware( thunk ) | ||
| loadInitialState( {}, getStateFromUrl(), reducer ), | ||
|
Member
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. this is a nice setup for passing this information; I appreciate the thought that went into your design |
||
| applyMiddleware( thunk, router ) | ||
| ); | ||
| persistState( store, reducer ); | ||
| store.dispatch( boot() ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
|
|
||
| import { REQUEST_TRIGGER, API_VERSIONS_RECEIVE, REQUEST_SELECT_ENDPOINT } from './actions'; | ||
| import { defaultState as defaultRequestState } from './request/reducer'; | ||
| import { defaultState as defaultUiState } from './ui/reducer'; | ||
| import { loadEndpoints } from './endpoints/actions'; | ||
| import { getEndpoints } from './endpoints/selectors'; | ||
|
|
||
| function getUrlParams() { | ||
| return new URL( window.location.href ).searchParams; | ||
| } | ||
|
|
||
| function encodeString( val ) { | ||
| return ( val === null || val === undefined ) ? '' : encodeURIComponent( val ); | ||
| } | ||
|
|
||
| function encodeObject( val ) { | ||
| return ( typeof val === 'object' ) && Object.keys( val ).length === 0 ? '' : encodeURIComponent( JSON.stringify( val ) ); | ||
| } | ||
|
|
||
| function decodeString( val ) { | ||
| return decodeURIComponent( val ); | ||
| } | ||
|
Member
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. is this offering much over directly calling
Contributor
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. It seems that it's making a pair between |
||
|
|
||
| function decodeObject( val ) { | ||
| return JSON.parse( decodeURIComponent( val ) ); | ||
| } | ||
|
|
||
| export const getUrlFromState = state => { | ||
|
|
||
| const { request, ui } = state; | ||
|
|
||
| const params = { | ||
| bodyParams: encodeObject( request.bodyParams ), | ||
| endpoint: encodeString( request.endpoint ? request.endpoint.pathLabeled : '' ), | ||
| method: encodeString( request.method ), | ||
| pathValues: encodeObject( request.pathValues ), | ||
| queryParams: encodeObject( request.queryParams ), | ||
| url: encodeString( request.url ), | ||
| api: encodeString( ui.api ), | ||
| version: encodeString( ui.version ), | ||
| }; | ||
|
|
||
| // Discard empty params | ||
| Object.keys( params ).forEach( key => ! params[ key ] && ( delete params[ key ] ) ); | ||
|
Member
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. there's a value in this terseness, but we don't often use the pattern of const urlParams = new URLSearchParams();
for ( const [ value, key ] of Object.entries( params ) ) {
if ( value ) {
urlParams.update( key, value );
}
}also is the basic truthiness check valid here for all the parameters? it seems like we might end up needlessly encoding empty objects and omit I wonder if it would help to use
Contributor
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. Nit: |
||
|
|
||
| // Construct url | ||
| const urlParams = new URLSearchParams( params ); | ||
| const url = window.location.origin + window.location.pathname + '?' + urlParams.toString(); | ||
|
Member
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. I think we can create a const url = new URL( window.location );
url.search = urlParams;
return url; |
||
|
|
||
| return url; | ||
| }; | ||
|
|
||
| export const getStateFromUrl = () => { | ||
| const urlParams = getUrlParams(); | ||
|
|
||
| if ( urlParams.toString() === '' ) { | ||
| return false; | ||
| } | ||
|
Member
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. there's a if ( urlParams.size === 0 ) {
return false;
} |
||
|
|
||
| const state = { request: { ...defaultRequestState }, ui: { ...defaultUiState } }; | ||
|
Member
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. it would be best to avoid such deep coupling outside of the react tree. I can easily imagine someone adding another top-level state property and not realizing this code needs it. I'm not sure we even need to return the full state object here as it seems like we could treat this as state augmentation. we can pass this into the initial state tree and merge it with the defaults there. that way these two parts of the system can operate independently. |
||
|
|
||
| try { | ||
| for ( const [ key, value ] of urlParams.entries() ) { | ||
| switch ( key ) { | ||
| case 'bodyParams': | ||
| state.request.bodyParams = decodeObject( value ); | ||
| break; | ||
| case 'method': | ||
| state.request.method = decodeString( value ); | ||
| break; | ||
| case 'pathValues': | ||
| state.request.pathValues = decodeObject( value ); | ||
| break; | ||
| case 'queryParams': | ||
| state.request.queryParams = decodeObject( value ); | ||
| break; | ||
| case 'url': | ||
| state.request.url = decodeString( value ); | ||
| break; | ||
| case 'api': | ||
| state.ui.api = decodeString( value ); | ||
| break; | ||
| case 'version': | ||
| state.ui.version = decodeString( value ); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } catch ( e ) { | ||
| // Fail without breaking the app | ||
| console.error( 'Could not parse state from url params.', e ); | ||
| return false; | ||
| } | ||
|
Member
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. it's a choice to bail if any parameter doesn't decode, but would we want to try and pre-fill whatever parameters we can decode? |
||
|
|
||
| return state; | ||
| }; | ||
|
|
||
| const router = ( { getState, dispatch } ) => { | ||
|
|
||
| // Determine if we need to load and select the endpoint | ||
| let isInitializingEndpoint = false; | ||
|
|
||
| const { ui = {} } = getStateFromUrl(); | ||
| const endpointUrlParam = getUrlParams().get( 'endpoint' ); | ||
| const endpointPathLabeled = endpointUrlParam ? decodeURIComponent( endpointUrlParam ) : false; | ||
| const apiName = ui.api; | ||
| const apiVersion = ui.version; | ||
|
|
||
| if ( endpointPathLabeled && apiName && apiVersion ) { | ||
| loadEndpoints( apiName, apiVersion )( dispatch ); | ||
| isInitializingEndpoint = true; | ||
| } | ||
|
|
||
| return next => action => { | ||
| const state = getState(); | ||
|
|
||
| switch ( action.type ) { | ||
| case REQUEST_TRIGGER: | ||
| const url = getUrlFromState( state ); | ||
| window.history.pushState( {}, document.title, url ); | ||
|
Member
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. MDN suggests that the middle parameter is unused, so passing a real value could be confusing since it won't have an impact in the browser. should we pass |
||
| break; | ||
| case API_VERSIONS_RECEIVE: | ||
| // Once the endpoint is loaded, select the endpoint. | ||
| if ( isInitializingEndpoint ) { | ||
| const endpoints = getEndpoints( state, apiName, apiVersion ); | ||
| const endpoint = endpoints.find( ( { pathLabeled } ) => pathLabeled === endpointPathLabeled ); | ||
| if ( endpoint ) { | ||
| dispatch( { type: REQUEST_SELECT_ENDPOINT, payload: { endpoint } } ); | ||
| } | ||
| isInitializingEndpoint = false; | ||
| } | ||
| break; | ||
|
Member
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. nice queueing of the initial request. |
||
| default: | ||
| break; | ||
| } | ||
|
|
||
| return next( action ); | ||
| }; | ||
| }; | ||
|
|
||
| export default router; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,12 @@ import { UI_SELECT_API, UI_SELECT_VERSION } from '../actions'; | |
| import { getDefault } from '../../api'; | ||
| import schema from './schema'; | ||
|
|
||
| const reducer = createReducer( { api: getDefault().name, version: null }, { | ||
| export const defaultState = { | ||
| api: getDefault().name, | ||
| version: null, | ||
| }; | ||
|
|
||
| const reducer = createReducer( defaultState, { | ||
|
Member
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. given that the reducer ultimately governs its own state and structure, I would feel better if this code merged the initial values from cache and from the URL, and then we could leave both of those systems decoupled from each other. this would require making this export something like export const makeReducer = ( { persistedState = null, urlState = null } ) => {
const initialState = Object.assign(
{},
defaultState,
persistedState,
);
if ( urlState ) {
initialState.request.bodyParams = urlState.bodyParams;
initialState.request.method = urlState.method;
…
}
return createReducer( initialState, { … } );
} |
||
| [ UI_SELECT_API ]: ( state, { payload } ) => { | ||
| return ( { | ||
| version: 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.
was this inclusion intentional? the
package.jsonalready specifiesnode@>=16.0.0but this one is more specific, and doesn't referencenpm