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

Create new Questionnaire (integration) #140

Merged
merged 38 commits into from
Jun 28, 2017
Merged

Conversation

hamishtaplin
Copy link
Contributor

@hamishtaplin hamishtaplin commented Jun 14, 2017

Changes

Added clientside logic for connecting to Author API:

  • Integrated Apollo Client
  • Removed questionnaire meta from Redux
  • Moved update logic to form input/textarea/select components
  • Connected frontend to Apollo backend
  • Updated breadcrumbs to work with backend
  • Added tests

Testing

  • Check process of adding a Questionnaire works and is stored in backend

onChange: PropTypes.func
history: PropTypes.object.isRequired, // eslint-disable-line
loading: PropTypes.bool.isRequired,
onUpdate: PropTypes.func.isRequired,
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need error propType here?

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 21, 2017

Choose a reason for hiding this comment

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

@bennpearson currently not dealing with errors, probably need to at some point though: https://trello.com/c/oQ5Nn9ll/88-error-handling

Copy link
Contributor

@WickyNilliams WickyNilliams left a comment

Choose a reason for hiding this comment

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

Looks good, couple of small points

type: PropTypes.oneOf(["text", "checkbox", "radio"]).isRequired,
value: PropTypes.oneOfType([
defaultValue: PropTypes.oneOfType([
Copy link
Contributor

Choose a reason for hiding this comment

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

why defaultValue is you're now using onChange handlers?

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 21, 2017

Choose a reason for hiding this comment

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

If we use value then you're depending on a (debounced) roundtrip to the api to update the value—makes for a rather laggy typing experience.

Using defaultValue sets the value on initial render but then the state of the value is handled internally, with updates being periodically sent to the API.

Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't be saving as a user types. The value of the form shouldn't be tied to a request to the server. It should go into the store, then pulled out when necessary.
In any case, saving as a user types opens up corner cases like rollback happening as user types - would actually cause them to lose work!

I personally think we should do auto-saving as a separate story rather than ad-hoc, as that tends to result in a better solution (e.g. perhaps it can be done cleanly with middleware?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we address this as a separate "saving/auto-saving" story when we have some more UI to work with?

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 that's wise. How about changing to autosave on blur for now? Conceptually simpler and less corner cases

const route = getRouteByPath(pathname);
const getQuestionnaire = gql`
query GetQuestionnaire {
questionnaire(id: 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be paramterised

Copy link
Contributor

Choose a reason for hiding this comment

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

I paramterised the gql on Publisher, see here for an example.

const route = getRouteByPath(pathname);
const getQuestionnaire = gql`
query GetQuestionnaire {
questionnaire(id: 1) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the breadcrumb requesting this data? Seems like an odd place for the request to be made. Especially when CreateQuestionnaire seems to do the same. Seems like the type of thing that should flow down through props

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 wasn't entirely happy with this but the alternative is a significant refactor. Structurally, CreateQuestionnaire and the Breadcrumbs are in different timezones, the data fetching would have to be done at layout level and then the props passed down through several layers of components, which comes with its own problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

Might be worth revisiting the architecture at some point. Particularly at which "level" we use container components. The Layout/Page hierarchy was something I threw together to get it working initially. If it is causing problems then we can revisit it. But it's probably outside the scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

The value goes into the store, presumably? So couldn't Breadcrumb be connected to store and pull the value out of there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@samiwel I think we should refactor if it becomes an issue. At the moment it's not causing any problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm yeah. But that was reading directly from the store and being dependent on the keying strategy used. Though there are methods for pulling stuff from the store without hitting the server: http://dev.apollodata.com/core/read-and-write.html. readQuery or readFragment are what we want, I think

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 22, 2017

Choose a reason for hiding this comment

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

Wasn't aware of that, I'll look into it. However, does't that rely on the request having been made elsewhere? Eg. if you navigate straight to the /design page there's no request currently so the breadcrumbs won't have a clue what's going on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it would rely on that. Haha, guess this goes back to the id in the url issue. OK let's leave for now. Do you want to add some tasks to trello to cover all the stuff we're going to defer for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking ahead—the Header/Nav will probably need to be more connected. I think that will present opportunities to connect higher up and pass things like title down to Breadcrumbs.

Copy link
Contributor

Choose a reason for hiding this comment

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

That sounds sensible. I wouldn't be surprised if it eventually ends up connected at page level, with everything flowing down from there

navigation,
legalBasis,
theme,
__typename
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need this in your query. Is there a reason for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was added whilst attempting to get MockedProvider to work in the tests. See: apollographql/react-apollo#674 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Did it make it work? If not, can it be removed?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ignore, I see you removed it

`;

const mapResultsToProps = results => {
const { data: { loading, questionnaire } } = results;
Copy link
Contributor

Choose a reason for hiding this comment

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

if you're going to do deep destructuring, what do you think about formatting it so it mirrors the structure of the object? e.g.

const {
    data : {
       loading,
       questionnaire
  }
} = results;

alternatively:

const { loading, questionnaire } = results.data;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Prefer the latter 👍


const mapResultsToProps = results => {
const { data: { loading, questionnaire } } = results;
// console.log(results.data.error);
Copy link
Contributor

Choose a reason for hiding this comment

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

remove pls :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops

);
export const withData = graphql(getQuestionnaire, {
props: mapResultsToProps,
options: { variables: { id: 1 } }
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't have the id hard-coded

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 21, 2017

Choose a reason for hiding this comment

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

Where should this live? We have to hardcode it somewhere, I think as we're only loading one questionnaire.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you create a function called withData which accepts a parameter (the Id) and returns the graphql? e.g. something like:

const withData = questionnaireId => {
  return graphql(getQuestionnaire, {
    props: mapResultsToProps,
    options: { variables: { id: questionnaireId } }
  });
}

Then call with withData(1)

Copy link
Contributor

@WickyNilliams WickyNilliams Jun 22, 2017

Choose a reason for hiding this comment

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

The id should come from the url i guess? When the questionnaire obj is in memory, you should have its id available

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 22, 2017

Choose a reason for hiding this comment

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

@samiwel This just moves the hardcoded value elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@WickyNilliams That would work but in the interests of moving PRs on a bit quicker, I think it's better that we solve problems (such as being able to dynamically select a questionnaire) when we need that feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

The URL stuff we could do later as that's a bigger task. But for now, you could pass the id back from the component?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's still just hardcoding the value elsewhere, solving a problem we don't have yet. We likely won't be passing that id in from the component in future so it will require another refactor further down the line.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean it should receive as prop and pass back? Or the callback could grab the value from somewhere before calling mutate. Either way, I absolutely think we be shouldn't be hardcoding values now

export class CreateQuestionnairePage extends Component {
constructor(props) {
super(props);
this.debouncedChangeHandler = debounce(this.onChange.bind(this), 300, {
Copy link
Contributor

Choose a reason for hiding this comment

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

surprised this needs binding if you're using a property initializer below, since that should already be bound to this

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 also surprised but it doesn't work otherwise, I think related to this answer on SO.

Copy link
Contributor

@WickyNilliams WickyNilliams Jun 22, 2017

Choose a reason for hiding this comment

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

Hmm, that answer suggested this should work:

   onChange = debounce(value => {
      this.setState(value, () => {
        this.props.onUpdate(this.state);
      });
    }, 300, { leading: true });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@WickyNilliams that was the first thing I did, it doesn't work. :(

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, screw it then. Leave as-is for now!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving to update on blur just made this irrelevant anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

All part of my master plan

dr-evils-laughing

Copy link
Contributor

Choose a reason for hiding this comment

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

"La-zer"

</ActionButtonGroup>
{loading
? <div>Loading...</div>
: <Form handleSubmit={this.onSubmit}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you extract the two branches into methods? Much clearer to read loading ? this.renderLoadingIndictator() : this.renderForm() than a huge block of JSX

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This just moves the huge chunk of JSX problem elsewhere, I've done it anyway though :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Haha fair point :) But a huge chunk of JSX isn't a problem in itself, it's just not great hanging off a ternary operator.

const createReducer = reducers =>
combineReducers({
questionnaire,
router: routerReducer,
Copy link
Contributor

Choose a reason for hiding this comment

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

could always put { routerReducer as router } in your import statement if you want to consistently use shorthand properties

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

return null;
},
networkInterface: createNetworkInterface({
uri: "http://localhost:4000/graphiql"
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be an environment variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It bloody should be 🎉

@bennpearson bennpearson dismissed their stale review June 22, 2017 10:54

Separate Trello task added to cover it

@@ -1 +1,2 @@
REACT_APP_BASE_NAME="/eq-author"
REACT_APP_API_URL=""
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we able to override the REACT_APP_API_URL value with an environment variable. Or do we need to default it to process.env.REACT_APP_API_URL here?

We need to be able to customise this for successful deployment to CloudFoundry.

Copy link
Contributor

@WickyNilliams WickyNilliams left a comment

Choose a reason for hiding this comment

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

This is so much simpler than we had before! Definitely moving in the right direction 👍

Few little points i think are worth fixing

@@ -87,11 +88,13 @@
"dependencies": {
"connected-react-router": "^4.2.1",
"enzyme": "^2.8.2",
"global": "^4.3.2",
Copy link
Contributor

Choose a reason for hiding this comment

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

What's this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

An accident!

import { ApolloClient, createNetworkInterface } from "react-apollo";

const client = new ApolloClient({
ddTypename: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

s/ddTypename/addTypename

hasUtilityBtns: false
};

BaseLayout.displayName = "BaseLayout";
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the need for this?

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 28, 2017

Choose a reason for hiding this comment

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

A workaround for this bug which breaks snaphot tests on Travis.

return (
<BreadcrumbNav aria-label="breadcrumb">
<BreadcrumbList>
<BreadcrumbItem key="/">
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't need key prop, unless elements are in an array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True

})
).isRequired
<BreadcrumbItem key={breadcrumb.path}>
<BreadcrumbLink to={breadcrumb.path}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should have the aria-current attribute on the current breadcrumb

Copy link
Contributor Author

@hamishtaplin hamishtaplin Jun 28, 2017

Choose a reason for hiding this comment

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

Linter complains about using aria-current, apparently it's not in the spec

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm that's odd. Appears to be part of the standard here https://www.w3.org/TR/wai-aria-1.1/#aria-current. Perhaps we should raise a bug against the eslint plugin?

id: PropTypes.string,
handleChange: PropTypes.func,
Copy link
Contributor

Choose a reason for hiding this comment

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

onChange should be the name of this kind of prop (here and anywhere else), to align with regular DOM components. handleChange should only be used internally e.g.

class ExampleComponents extends Component {

  handleChange = (e) {
    const value = e.target.value;
    this.props.onChange(value);
  };

  render() {
    return (
      <input
        type="text"
        value={this.props.value}
        onChange={this.handleChange}
      /> 
    );
  }

}

return (
<Center>
<Panel>
<Form handleSubmit={handleSubmit}>
Copy link
Contributor

Choose a reason for hiding this comment

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

as before, this should be onSubmit. onX for external props, handleX for internal method

this.setState(questionnaire);
}

onChange = value => this.setState(value);
Copy link
Contributor

Choose a reason for hiding this comment

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

as discussed, these should be called handleChange, handleSubmit etc

this.props.onUpdate(this.state);
};

onSubmit = e => e.preventDefault();
Copy link
Contributor

Choose a reason for hiding this comment

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

handleX


export default compose(connect(mapStateToProps), withData, withMutation)(
QuestionnaireMeta
);
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a lot of duplicated code between this and src/containers/QuestionnaireDesignPage/index.js. Should extract common queries etc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think the best way to do so is to implement the webpack loader plugin, can this be done as a separate task?

Copy link
Contributor

Choose a reason for hiding this comment

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

@WickyNilliams
Copy link
Contributor

Create New Q (8)

@@ -3,11 +3,11 @@ import { shallow } from "enzyme";
import { Form } from "components/Forms/Form";

let wrapper;
const handleSubmit = jest.fn();
const onSubmit = jest.fn();
Copy link
Contributor

Choose a reason for hiding this comment

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

These functions should actually be called handleSubmit haha. But don't worry if they're just tests!

@@ -52,7 +52,7 @@ export default class RichTextArea extends Component {
};
}

handleChange = value => {
onChange = value => {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be handleChange

@hamishtaplin hamishtaplin merged commit 85da995 into master Jun 28, 2017
@hamishtaplin hamishtaplin deleted the 2/create-new-questionnaire branch June 28, 2017 13:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants