Skip to content

Spike adding field validation UI feedback

Andrew Price edited this page May 15, 2019 · 9 revisions

Introduction

There is a requirement to prevent surveys launching when there are still validation errors.

https://trello.com/c/7DKHeOHG/1067-spike-validator-question-title-1-day

Scope

This spike will investigate a possible approach and provide a PoC. This builds on a previous investigative spike: https://github.com/ONSdigital/eq-author-app/wiki/Validation-for-Authors-Spike and is based on prototype https://author.netlify.com/#/eq-author/

The PoC is on this branch: https://github.com/ONSdigital/eq-author-app/compare/1067-spike-validator-question-title-1-day

Specifically, we need to address:

  1. How error validations should be represented in the schema
  2. How a page/answer decides if something is invalid
  3. How a question page calculates & displays the total number of errors
  4. How a new question page is identified - a newly created page doesn't immediately show validation errors
  5. How a new answer is identified - a newly created answer doesn't immediately show validation errors
  6. How question page metadata is validated
  7. How an answer is validated
  8. How the Launch Survey button is only enabled when there are no validation errors

1. How error validations should be represented in the schema

A previous spike https://github.com/ONSdigital/eq-author-app/wiki/Validation-for-Authors-Spike and is based on prototype https://author.netlify.com/#/eq-author/ looked at different approaches to storing the validation. Two options considered are:

  1. Raise an Error when invalid data submitted - this prevents invalid data from being stored. Not technically what we want as we need the user to be able to save invalid data, just not to be able to launch a survey
  2. Create a validationError property on the question/answer objects - this allows invalid data to be saved and relies on UI to report on validation errors

A new GraphQL fragment has been created to represent validation errors:

fragment ValidationErrorInfo on QuestionPage {
  validationErrorInfo {
    errors {
      field
      message
    },
    totalCount
  }
}

where totalCount = number of errors on page + child answers.

2. How a page/answer decides if something is invalid

This is calculated in the question page resolver on api. Logic is currently done within component but possible rule engine or separate component would be more beneficial?

const questionPageValidation = page => {
  const errors = [];
  
  /* Page validation done here - maybe a better way of doing it? Rule engine, separate component? */
  if (!page.title) {
    errors.push({
      field: "title",
      message: "Question title is required",
    });
  }

  // Add total answer validation errors
  const answerErrorCount = page.answers.reduce((acc, answer) => {
    return answer.validationErrorInfo && answer.validationErrorInfo.totalCount
      ? acc + answer.validationErrorInfo.totalCount
      : acc;
  }, 0);

  return {
    errors,
    totalCount: errors.length + answerErrorCount,
  };
};

Resolvers.QuestionPage = {
  ...
  validationErrorInfo: page => questionPageValidation(page),
}

eq-author-api/schema/resolvers/pages/questionPage.js

ISSUE: In questionPageValidation, page.answers has list of answers but without their corresponding validationErrorInfo object. How to access validationErroInfo at this point? This prevents the answers contributing to an invalid page.

3. How a question page calculates & displays the total number of errors

The totalCount property calculated in 2. above contains the total number of validation errors on the question page. Adding the validationErrorInfo property to graphql query pageNavItem will return total error count in page

UnwrappedPageNavItem.fragments = {
  ...
  validationErrorInfo {
    totalCount
  }

...

// The navigation item can display to total count if necessary:
<NavLink>
  {page.validationErrorInfo.totalCount
    ? `${page.displayName} (${page.validationErrorInfo.totalCount})`
    : page.displayName}
</NavLink>

eq-author/src/App/QuestionnaireDesignPage/NavigationSidebar/PageNavItem.js

4. How a new question page is identified

A newly created question page does not show the validation errors immediately so we need to identify it's newness. Apollo v2.5 introduced local stage management which can be used to store data client side: https://www.apollographql.com/docs/react/essentials/local-state

Client side resolvers are created to track new pages/answers etc

This defines additional schema properties, queries & mutations available only on the client.

e.g.

export const resolvers = {
   Mutation: {
      createQuestionPage: (_, input, { cache }) => {
         cache.writeData({ data: { newPageId: _.createQuestionPage.id } });
      },
   }
}

Writes the latest created question page id to local cache. This can then be used in the page query to create an isNew property on the page object

const GET_NEW_PAGE_ID = gql`
  query newPageId {
    newPageId @client
  }
`;

export const resolvers = {
  QuestionPage: {
    isNew: (questionPage, _, { cache }) => {
      const { newPageId } = cache.readQuery({ query: GET_NEW_PAGE_ID });
      if (questionPage.id !== newPageId) {
        // Reset new page id
        cache.writeData({
          data: {
            newPageId: null,
            newAnswerId: null,
            newOptions: [],
          },
        });
      }
      return questionPage.id === newPageId;
    },
  },

  Mutation: {
     createQuestionPage: (_, input, { cache }) => {
        cache.writeData({ data: { newPageId: _.createQuestionPage.id } });
     },
  }
}

See https://github.com/ONSdigital/eq-author-app/compare/1067-spike-validator-question-title-1-day?expand=1#diff-f1d78d9edde1c83b437cacbc9f770ca3 for working example.

5. How a new answer is identified - a newly created answer doesn't immediately show validation errors

Similar to a page, an answer needs to be identified as new. This can be done in answersEditor component which can track if an answer already exists when rendering. eq-author/src/App/page/Design/QuestionPageEditor/AnswersEditor/index.js can keep a list of answersIds which is updated after rendering. The render method labels the answerEditor component as new if it doesn't exists.

6. How the Launch Survey button is only enabled when there are no validation errors`

The launch survey button can be disabled by checking if any validation errors exist in any of the pages

canLaunchSurvey = () => {
    let surveyValid = true;

    this.props.questionnaire.sections.forEach(section => {
      section.pages.forEach(page => {
        if (
          page.validationErrorInfo &&
          page.validationErrorInfo.totalCount > 0
        ) {
          surveyValid = false;
        }
      });
    });

    return surveyValid;
  };

eq-author/src/components/Header/index.js

Clone this wiki locally