Skip to content

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

yansern
Copy link
Contributor

@yansern yansern commented Sep 14, 2023

Update: I didn't realize there was another PR working on a similar feature here until I submitted this PR. #106

Update 2: I looked into the other PR and I would say the approach taken in this PR is more feature complete. This PR supports restoring the endpoint selected from suggestion dropdown, and the path values of those selected endpoint and also the query params from filled from the UI.

Description

This PR generates a shareable URL and pushes the browser history state when the "Run Endpoint" button is clicked, allowing the URL to be copied from the address bar and shared to colleagues.

In order to achieve this, I created a redux middleware called router that would encode and decode relevant states to and from URLs.

Test Instructions

  1. You can test this with the following urls and you can see the values prefilled:

  2. Test entering the URLs signed-out, and then sign-in. After signing-in, you should be redirected back to the same url with all the params intact.

Additional notes

  • The endpoint doesn't automatically run on page load. It only prefills everything for you. User is expected to click on the "Run Endpoint" button themselves. :)
  • Pressing "Back" currently does not restore previous state as I foresaw additional complexity around this when trying to implement this and I thought its best left for another PR. User can just refresh the page for now.

Screenshots

image image

@yansern yansern self-assigned this Sep 14, 2023
Copy link

@TimBroddin TimBroddin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works, doesn't break anything, and adds great functionality. LGTM!

@valterlorran
Copy link

I have yet to test it, but it looks great! It's way simpler than my PR. 👍

@yansern
Copy link
Contributor Author

yansern commented Sep 23, 2023

@TimBroddin discovered that the existing implementation of state persistence needsStoring = nextState === state; is most likely incorrect, as it should be needsStoring = nextState !== state;.

At the same time, I'd like to ensure that loading initial state from param also takes in considering of existing data in local storage, which may already have endpoints data loaded, which will shave off a couple of seconds on repeat pageload.

Will continue refining this PR. We're in no rush.

Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yansern this is a great patch. I left a lot of comments, but the ones that I think are important are questions about deep coupling between app components. I think we can make a few adjustments and avoid some headaches down the line. thanks for being patient with me!

@@ -0,0 +1 @@
16.20.2
Copy link
Member

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.json already specifies node@>=16.0.0 but this one is more specific, and doesn't reference npm

@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The 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?

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 ),
Copy link
Member

Choose a reason for hiding this comment

The 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

};

// Discard empty params
Object.keys( params ).forEach( key => ! params[ key ] && ( delete params[ key ] ) );
Copy link
Member

Choose a reason for hiding this comment

The 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 ** for control flow, and given how infrequently this project is maintained, it might be more valuable to choose something more familiar, including constructing the object positively instead of removing empty bits.

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 false values.

I wonder if it would help to use undefined to include these when forming the params object. url: '' === request.url ? encodeString( request.url ) : undefined

Copy link
Contributor

@mreishus mreishus Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: const [ value, key ] of Object.entries( params ) should be const [ key, value ] of Object.entries( params ), or weird things start happening. Also .update -> .set.


function decodeString( val ) {
return decodeURIComponent( val );
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this offering much over directly calling decodeURIComponent()? I find that the abstraction hasn't made the code clearer to me, and from the way it's used, it seems like it's only being used in the context of a query string where decodeURIComponent is a fitting match.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that it's making a pair between encodeString and decodeString, it's clear that those do the inverse of each other. encodeObject and decodeObject is a complementary pair. Replacing one of those 4 with a direct call makes it more confusing imo.

return false;
}

const state = { request: { ...defaultRequestState }, ui: { ...defaultUiState } };
Copy link
Member

Choose a reason for hiding this comment

The 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.

// Fail without breaking the app
console.error( 'Could not parse state from url params.', e );
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The 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?

switch ( action.type ) {
case REQUEST_TRIGGER:
const url = getUrlFromState( state );
window.history.pushState( {}, document.title, url );
Copy link
Member

Choose a reason for hiding this comment

The 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 '' here instead?

}
isInitializingEndpoint = false;
}
break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice queueing of the initial request.
do we need to make allotments for logged-out views here or will this only run if someone is logged in?

version: null,
};

const reducer = createReducer( defaultState, {
Copy link
Member

Choose a reason for hiding this comment

The 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 makeReducer that takes both of those and merges them.

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, { … } );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants