Skip to content
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

Happychat: Rework selected site logic to send proper site info to happychat and kayako #15013

Merged
merged 3 commits into from
Jun 15, 2017

Conversation

unDemian
Copy link
Contributor

This PR moves selected site logic to an upper level as prop of the help component. This will provide us with the selected site info which are sent when initializing a new chat or creating a kayako

Testing steps

  1. Starting a new chat or kayako ticket should have proper site url
  2. Unit tests should not fail

Implementation description

  • redux/help.selectedSiteId was connected to help component as a prop
  • A new selector was added to help/selectors that will provide site info for the selected site or if a site is not selected for the primary site.
  • selectedSite is passed down as a common prop to help-contact-form

Fixes: #14997

@matticbot
Copy link
Contributor

@matticbot matticbot added the [Size] M Medium sized issue label Jun 12, 2017
@unDemian unDemian added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jun 12, 2017
@dllh dllh requested a review from omarjackman June 12, 2017 21:53
@@ -38,6 +36,7 @@ export const HelpContactForm = React.createClass( {
showSubjectField: PropTypes.bool,
showSiteField: PropTypes.bool,
showHelpLanguagePrompt: PropTypes.bool,
selectedSite: PropTypes.object,
Copy link
Member

Choose a reason for hiding this comment

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

Since we're only using the ID can we just pass selectedSiteId instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking that if we ever need any other site details in the future they'll be there, I guess I could just send the id for now and we could rework it later.

Copy link
Member

@mattwondra mattwondra left a comment

Choose a reason for hiding this comment

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

Hm, sorry to put a block on this, but I'm not sure I think this is the best approach here.

The contact form gathers information from the user and then passes this information up when the form is submitted. So it seems odd to me that in this implementation, even though the contact form is setting the selected site, the parent is reaching into Redux state for that data.

The bug appeared when siteId was removed from this.state in the Contact Form. Isn't the simplest solution to keep the Contact Form in charge of passing that data back on submit?

I think submitForm in the Contact Form should be changed — instead of naively passing back this.state (including any new state that we actually don't want the parent to know about) we should explicitly build up what we're passing back:

submitForm() {
	const {
		howCanWeHelp,
		howYouFeel,
		message,
		subject
	} = this.state;
	
	this.props.onSubmit( {
		howCanWeHelp,
		howYouFeel,
		message,
		subject,
		siteId: this.props.selectedSiteId,
	} );
},

This makes the contract between the component and its parent much clearer — you'll get these params back when the form is submitted. I haven't tested, but I think this is the only code you need to modify to fix the bug.

I just worry about the Contact Form being responsible for setting the site, but relying on the parent to send the selected site back in as props. But I'm just making this review a "Comment" in case there's something I'm missing, or there's even a better way than my suggestion.

Copy link
Contributor

@omarjackman omarjackman left a comment

Choose a reason for hiding this comment

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

I made a note about a regression in your changes that causes the user to always default to their primary site but I think I mostly agree with comment @mattwondra left.

import createSelector from 'lib/create-selector';

export const getHelpSelectedSiteId = createSelector(
state => state.help.selectedSiteId
);

export const getHelpSelectedSite = ( state ) => {
const siteId = getHelpSelectedSiteId( state ) || getPrimarySiteId( state );
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you should be using getSelectedOrPrimarySiteId here instead of getPrimarySiteId. Without the form will always default to the users primary site instead of the site they currently have selected

Copy link
Contributor Author

@unDemian unDemian Jun 13, 2017

Choose a reason for hiding this comment

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

Nice, I didn't notice that it was added. Updated, thanks.

@unDemian
Copy link
Contributor Author

unDemian commented Jun 13, 2017

Hm, sorry to put a block on this, but I'm not sure I think this is the best approach here.

No worries, that is why we do reviews.

The contact form gathers information from the user and then passes this information up when the form is submitted. So it seems odd to me that in this implementation, even though the contact form is setting the selected site, the parent is reaching into Redux state for that data.

I agree, (the only thing is that with #13520 we introduced a dependency on having siteId in the help page before the form is submitted, Happychat button might not even be active if the site you selected is part of a group that has no online operators -- partly true ).
Another reason was that I wanted to not use sites.getSite anymore as it is deprecated. I guess I could get the site info in the contact form's mapStateToProp and submit it to help page component. Thoughts?

I think submitForm in the Contact Form should be changed — instead of naively passing back this.state (including any new state that we actually don't want the parent to know about) we should explicitly build up what we're passing back

Definitely, this could be a future enhancement but is not part of the scope of this PR. Not sure if we should do it part of this issue as it was a fairly simply high priority thing that we need to ship asap.

The bug appeared when siteId was removed from this.state in the Contact Form. Isn't the simplest solution to keep the Contact Form in charge of passing that data back on submit?

That is how I started but then I removed it after our discussion here https://github.com/Automattic/wp-calypso/pull/13520/files/27face45b7fb423c570d08cd1c369d08cbdf285b#diff-bc7120f8349b381d24b34e6e3a2daeb2 the thing is we still need the one in redux so we can catch the value in the connection middleware.

Thanks for taking the time to review this, I pushed some updates for the inline comments. Feel free to re-review.

@ockham
Copy link
Contributor

ockham commented Jun 13, 2017

Somewhat related discussion: #13520 (comment)

@ockham
Copy link
Contributor

ockham commented Jun 13, 2017

I'll chime in and say that this sort of logic duplicated from our site-related selectors IMO is a code smell indicating that this isn't the right approach. (Among other things, you probably want to use getSite instead of getRawSite, but that's really tangential to what I believe is an underlying issue.)

Copy link
Contributor

@ockham ockham left a comment

Choose a reason for hiding this comment

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

Agreeing with what @mattwondra said; this should be handled in HelpContactForm's component (not Redux) state.

@mattwondra
Copy link
Member

(the only thing is that with #13520 we introduced a dependency on having siteId in the help page before the form is submitted, Happychat button might not even be active if the site you selected is part of a group that has no online operators -- partly true ).

Can you help me understand this? I'm guessing what you mean is, without HelpContact knowing a siteId ahead of time, Happychat assumes the user is in the WP.com — then when the form loads, if the primary site is a JPOP site Happychat will be notified and there may actually not be any JPOP HEs in Happychat. The user experience would be a flash of the Happychat form, then it would switch to the Kayako form. Not ideal, but I also wonder if this could be mitigated with better overall architecture of the existing components you're working with...

Ultimately this should probably just get shipped in some form, because this whole component structure needs a refactor and that shouldn't hold this stuff up. I'd like to see clearer component responsibility, but these were not built with current use cases in mind so I'm fine with moving forward however gets this out the door.

@unDemian
Copy link
Contributor Author

unDemian commented Jun 13, 2017

Feedback Responses

Can you help me understand this? I'm guessing what you mean is, without HelpContact knowing a siteId ahead of time, Happychat assumes the user is in the WP.com — then when the form loads, if the primary site is a JPOP site Happychat will be notified and there may actually not be any JPOP HEs in Happychat.

Exactly, but I added the 'default' siteId in happychat connect which should almost solve the issue.

Among other things, you probably want to use getSite instead of getRawSite, but that's really tangential to what I believe is an underlying issue.

Thanks, updated.

Ultimately this should probably just get shipped in some form, because this whole component structure needs a refactor and that shouldn't hold this stuff up. I'd like to see clearer component responsibility, but these were not built with current use cases in mind so I'm fine with moving forward however gets this out the door.

I agree, this is not ideal but I would like to move forward with the issue at hand as it's affecting HEs, I created a refactor issue 573-gh-happychat where we can add more info and a proper estimate. And maybe someone with a better high level understanding of react-redux best practices can rework some of the components or maybe future me :)

New Approach

Considering feedback from here, #13520 and a private conversation that I had with @ockham I pushed some updates. Basically the help-contact-form grabs the siteId from redux and it's also the one that will update the redux value when a different site is selected. Afterwards the selected siteInfo is sent to help component on submit. I considered moving it as component state field of help like @ockham suggested but that would complicate things with async <HappyConnection /> and updatePreferences.

For now we need to have it in redux, and if I understood correctly if help-contact-form handles the updates it should make the components' responsibility a bit clearer.

Please re-review. And thanks a lot for helping me better understand how to structure react components.

@ockham
Copy link
Contributor

ockham commented Jun 13, 2017

I considered moving it as component state field of help like @ockham suggested but that would complicate things with async <HappyConnection /> and updatePreferences.

I tried to address how to drop updatePreferences here:

If obtaining additional site properties is an issue, and you'd like to avoid sites-list (as you should 😉), you could consider e.g. passing siteId instead of site.URL here, and changing the corresponding action to use the getSite() selector (after obtaining state here).

Not familiar with the <HappyConnection /> component, so I can't say much about that yet.

TBH, I'd still prefer if we were able to drop the extraneous Redux state (instead of introducing yet another selector)...

@unDemian
Copy link
Contributor Author

unDemian commented Jun 13, 2017

I tried to address how to drop updatePreferences here

Your example is referring to the initial message sent via sendInfo, however even for that moving the getSite inside the sendInfo would work for that case, but as can you see just a couple of rows afterwards we need the site information

site_plan_product_id: ( site ? site.plan.product_id : null ),
and we don't really have the state, here also
const site = sites.getSite( siteId );

By updatePreferences I was referring to sending preferences via socket whenever a different site is selected

export const updateChatPreferences = ( connection, { getState }, siteId ) => {

Honestly, I cannot see an easy way to not having it in redux. But I'm open to suggestions.

@mattwondra
Copy link
Member

Here's an annotated pseudo-code example of how to re-structure the responsibilities so that we don't need to store anything in Redux state. The overview:

  • Make <HelpContact> responsible for maintaining siteId in state. This means that on the initial Happychat connection (before the form loads) it already knows a default siteId to use to calculate groups.
  • <HelpContact> passes siteId to <HelpContactForm> as a prop. The form is no longer responsible for keeping track of siteId, it simply takes it as a prop and lets the parent know (via a prop callback) when the value changes.
  • Update <HappychatConnection> to take a groups prop. This component will be responsible for the details of group management. This means anywhere we have a <HappychatConnection> we can pass in the groups that should be available for that context, again without needing to store a global in Redux.
// MAIN HELP PAGE COMPONENT
// This component takes all responsibility for keeping track of the current siteId.
class HelpContact extends React.Component {
	constructor() {
		this.state = {
			siteId: '<Get default site ID from Redux, so initial groups are correct>'
		};
	}

	render() {
		// By passing the same siteId into <HappychatConnection /> and <HelpContactForm />,
		// we ensure that the site shown to the user always matches the groups Happychat
		// is connecting the user to.
		const { siteId } = this.state;
		return (
			<div>
				{ /*
					Calculate groups based on the current siteId and pass them into the
					<HappychatConnection />. This lets that component take all responsibility
					for setting and maintaining groups as siteId changes.
				*/ }
				<HappychatConnection groups={ this.props.getGroups( siteId ) } />

				{ /*
					Pass the siteId, tracked in my state, to the <HelpContactForm />, and
					update my state whenever the input's value changes. This is like treating
					the form as a multi-value controlled input.
				*/ }
				<HelpContactForm
					siteId={ siteId }
					onSiteChange={ this.handleSiteChange }
					...
				/>
			</div>
		)
	}

	handleSiteChange( siteId ) {
		// When the siteId changes in the form, update siteId in state.
		// This causes HappychatConnection to receive the new siteId as props.
		this.setState( { siteId } );
	}
}
// CONTACT FORM COMPONENT
// This component becomes very dumb and agnostic about siteId — the parent must
// pass it in and update when the dropdown makes a new selection.
class HelpContactForm extends React.Component {
	render() {
		// The SitesDropdown takes its selected site from props, and on changes
		// passes the new ID back to the parent, which will then update its state
		// and pass this back in as props.
		return (
			<div>
				...
				<SitesDropdown
					selectedSiteId={ this.props.siteId }
					onSiteSelect={ this.props.onSiteChange } />
				...
			</div>
		)
	}
}
// HAPPYCHAT CONNECTION COMPONENT
// Add groups management to this component. Pass in a groups prop, and this component
// ensures that Happychat always knows which group the user belongs to.
class HappychatConnection extends Component {
	componentDidMount() {
		if ( this.props.isUninitialized ) {
			// Update connectChat to take a list of groups, so the customer is put in
			// the right groups immediately.
			this.props.connectChat( { groups: this.props.groups } );
		} else {
			// If the chat has already been initialized, send the groups since they might
			// have changed since the initial connection.
			//
			// Create a new Redux action that fires HAPPYCHAT_SET_GROUPS with these
			// groups. Middleware can catch it and send the preferences to the socket.
			// We don't need to keep any of this in the Redux store though.
			this.props.setHappychatGroups( this.props.groups );
		}
	}

	componentWillReceiveProps( nextProps ) {
		// Whenever groups change, tell Happychat about it
		if ( this.props.groups !== nextProps.groups ) {
			this.props.setHappychatGroups( nextProps.groups );
		}
	}

	render() {
		return null;
	}
}

@mattwondra
Copy link
Member

Accidentally submitted my last comment before I finished:

It may seem like it's adding complication to pass siteId back and forth between the form and the parent — but this is a common React pattern, and it makes the bigger issue (managing changing Happychat groups, including on initial connection) so much simpler.

I don't love that the site selector is the only "controlled" input in that form — it might actually make sense to drop all state from the form and force all input values to be passed in and updated in the parent. But that's really not necessary to get this going, again considering that this whole suite of components is overdue for a refactor anyways.

I tried to make this as clear as possible, but I might have not explained something or just be wrong about how this will end up working. But this is the avenue I would explore.

@dllh
Copy link
Member

dllh commented Jun 15, 2017

We need to land some fix for this today, even if it's not the perfect fix. I'm happy to go back immediately afterward and spend some time getting the perfect fix in, but this is impacting our ability to provide quality, efficient service to our customers, and I'd rather we fix what's broken quickly and then go back and make it better than that we hold up further for a perfect fix the first time around at the cost of days of broken tools.

@unDemian
Copy link
Contributor Author

unDemian commented Jun 15, 2017

@mattwondra really appreciate your input, helps me a lot. I moved it to 573-gh-happychat so we can address it as soon as possible.

Could you guys take another look at the fix in place so we can move forward with this.
Thanks a lot. 🥇

Copy link
Member

@mattwondra mattwondra left a comment

Choose a reason for hiding this comment

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

Looks like this fixes the underlying bug with no noticeable side effects. As discussed elsewhere, this can deploy with iterations happening later as needed.

:shipit: :shipit: :shipit:

@unDemian unDemian merged commit 7ab996e into master Jun 15, 2017
@matticbot matticbot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jun 15, 2017
@ockham ockham mentioned this pull request Jul 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Happychat [Size] M Medium sized issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants