diff --git a/packages/icons/package.json b/packages/icons/package.json index df407b21..7d497a0a 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -13,7 +13,7 @@ "build:lib": "NODE_ENV=production redrun -p build:es build:umd", "build:bundle": "echo 0", "prepublish": "NODE_ENV=production redrun build:lib", - "lint": "redrun lint:ci -- -- --fix", + "lint": "redrun lint:ci -- --fix", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md", "test": "NODE_ENV=test joyent-react-scripts test --env=jsdom", "test:ci": "redrun test", diff --git a/packages/logos/package.json b/packages/logos/package.json index af8021ce..43aee564 100644 --- a/packages/logos/package.json +++ b/packages/logos/package.json @@ -13,7 +13,7 @@ "build:lib": "NODE_ENV=production redrun -p build:es build:umd", "build:bundle": "echo 0", "prepublish": "NODE_ENV=production redrun build:lib", - "lint": "redrun lint:ci -- -- --fix", + "lint": "redrun lint:ci -- --fix", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md", "test": "echo 0", "test:ci": "redrun test", diff --git a/packages/my-joy-images/package.json b/packages/my-joy-images/package.json index 267e64bc..697f5708 100644 --- a/packages/my-joy-images/package.json +++ b/packages/my-joy-images/package.json @@ -11,9 +11,9 @@ "build:lib": "echo 0", "build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr", "prepublish": "NODE_ENV=production redrun build:bundle", - "lint": "redrun lint:ci -- -- --fix", + "lint": "redrun lint:ci -- --fix", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md", - "test": "redrun test:ci", + "test": "echo 0", "test:ci": "echo 0", "build:frontend": "joyent-react-scripts build", "build:ssr": "SSR=1 UMD=1 babel src --out-dir lib/app --copy-files" @@ -42,7 +42,6 @@ "lodash.uniqby": "^4.7.0", "lunr": "^2.1.6", "param-case": "^2.1.1", - "punycode": "^2.1.0", "react": "^16.2.0", "react-apollo": "^2.0.4", "react-dom": "^16.2.0", diff --git a/packages/my-joy-images/src/containers/create-image/details.js b/packages/my-joy-images/src/containers/create-image/details.js index aec1e634..5e48d5d2 100644 --- a/packages/my-joy-images/src/containers/create-image/details.js +++ b/packages/my-joy-images/src/containers/create-image/details.js @@ -2,13 +2,12 @@ import React, { Fragment } from 'react'; import { compose, graphql } from 'react-apollo'; import { set } from 'react-redux-values'; import ReduxForm from 'declarative-redux-form'; +import { Row, Col } from 'joyent-react-styled-flexboxgrid'; import { Margin } from 'styled-components-spacing'; import { change } from 'redux-form'; import { connect } from 'react-redux'; import intercept from 'apr-intercept'; import get from 'lodash.get'; -import punycode from 'punycode'; -import { Row, Col } from 'joyent-react-styled-flexboxgrid'; import { NameIcon, H3, Button, H4, P } from 'joyent-ui-toolkit'; @@ -16,7 +15,8 @@ import Title from '@components/create-image/title'; import Details from '@components/create-image/details'; import Description from '@components/description'; import GetRandomName from '@graphql/get-random-name.gql'; -import createStore from '@state/apollo-client'; +import createClient from '@state/apollo-client'; +import { instanceName as validateName } from '@state/validators'; import { Forms } from '@root/constants'; const NameContainer = ({ @@ -27,7 +27,7 @@ const NameContainer = ({ description, placeholderName, randomizing, - handleAsyncValidation, + handleAsyncValidate, shouldAsyncValidate, handleNext, handleRandomize, @@ -54,9 +54,9 @@ const NameContainer = ({ form={Forms.FORM_DETAILS} destroyOnUnmount={false} forceUnregisterOnUnmount={true} - onSubmit={handleNext} - asyncValidate={handleAsyncValidation} + asyncValidate={handleAsyncValidate} shouldAsyncValidate={shouldAsyncValidate} + onSubmit={handleNext} > {props => expanded ? ( @@ -121,6 +121,7 @@ export default compose( ({ form, values }, ownProps) => { const name = get(form, `${Forms.FORM_DETAILS}.values.name`, ''); const version = get(form, `${Forms.FORM_DETAILS}.values.version`, ''); + const description = get( form, `${Forms.FORM_DETAILS}.values.description`, @@ -128,7 +129,6 @@ export default compose( ); const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false); - const randomizing = get(values, 'create-image-name-randomizing', false); return { @@ -141,35 +141,23 @@ export default compose( }; }, (dispatch, { history, match }) => ({ - shouldAsyncValidate: ({ trigger }) => trigger === 'submit', - handleAsyncValidation: async ({ name }) => { - const sanitized = punycode.encode(name).replace(/-$/, ''); - - if (sanitized !== name) { - // eslint-disable-next-line no-throw-literal - throw { - name: 'Special characters are not accepted' - }; - } - - if (!/^[a-zA-Z0-9][a-zA-Z0-9\\_\\.\\-]*$/.test(name)) { - // eslint-disable-next-line no-throw-literal - throw { - name: 'Invalid name' - }; - } - }, handleNext: () => { dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true })); - return history.push(`/~create/${match.params.instance}/tag`); }, - handleEdit: () => history.push(`/~create/${match.params.instance}/name`), + handleEdit: () => { + dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true })); + return history.push(`/~create/${match.params.instance}/name`); + }, + shouldAsyncValidate: ({ trigger }) => { + return trigger === 'change'; + }, + handleAsyncValidate: validateName, handleRandomize: async () => { dispatch(set({ name: 'create-image-name-randomizing', value: true })); const [err, res] = await intercept( - createStore().query({ + createClient().query({ fetchPolicy: 'network-only', query: GetRandomName }) diff --git a/packages/my-joy-images/src/containers/create-image/tags.js b/packages/my-joy-images/src/containers/create-image/tags.js index 25d7f640..9f346f3a 100644 --- a/packages/my-joy-images/src/containers/create-image/tags.js +++ b/packages/my-joy-images/src/containers/create-image/tags.js @@ -21,6 +21,7 @@ import { import Title from '@components/create-image/title'; import Description from '@components/description'; import Tag from '@components/tags'; +import { addTag as validateTag } from '@state/validators'; import { Forms } from '@root/constants'; export const Tags = ({ @@ -34,6 +35,8 @@ export const Tags = ({ handleToggleExpanded, handleCancelEdit, handleChangeAddOpen, + handleAsyncValidate, + shouldAsyncValidate, handleNext, step, handleEdit, @@ -84,6 +87,8 @@ export const Tags = ({ form={Forms.FORM_TAGS_CREATE} destroyOnUnmount={false} forceUnregisterOnUnmount={true} + asyncValidate={handleAsyncValidate} + shouldAsyncValidate={shouldAsyncValidate} onSubmit={handleAddTag} > {props => @@ -141,6 +146,10 @@ export default compose( handleEdit: () => { return history.push(`/~create/${match.params.instance}/tag`); }, + shouldAsyncValidate: ({ trigger }) => { + return trigger === 'submit'; + }, + handleAsyncValidate: validateTag, handleAddTag: value => { const toggleToClosed = set({ name: `${Forms.CREATE_TAGS}-add-open`, diff --git a/packages/my-joy-images/src/containers/tags.js b/packages/my-joy-images/src/containers/tags.js index b29b1e58..2d0d5ada 100644 --- a/packages/my-joy-images/src/containers/tags.js +++ b/packages/my-joy-images/src/containers/tags.js @@ -27,6 +27,7 @@ import Tag, { AddForm } from '@components/tags'; import ToolbarForm from '@components/toolbar'; import UpdateImageTags from '@graphql/update-image-tags.gql'; import GetTags from '@graphql/get-tags.gql'; +import { addTag as validateTag } from '@state/validators'; import parseError from '@state/parse-error'; const { TAGS_TOOLBAR_FORM, TAGS_ADD_FORM } = Forms; @@ -38,6 +39,8 @@ export const Tags = ({ error = null, mutationError = null, mutating = false, + handleAsyncValidate, + shouldAsyncValidate, handleToggleAddOpen, handleRemoveTag, handleAddTag @@ -76,7 +79,12 @@ export const Tags = ({ ) : null} - + {props => addOpen ? ( @@ -162,6 +170,10 @@ export default compose( }; }, (dispatch, { image, tags = [], updateTags, refetch }) => ({ + shouldAsyncValidate: ({ trigger }) => { + return trigger === 'submit'; + }, + handleAsyncValidate: validateTag, handleToggleAddOpen: addOpen => { dispatch(set({ name: `${image.id}-add-open`, value: addOpen })); }, diff --git a/packages/my-joy-images/src/state/validators.js b/packages/my-joy-images/src/state/validators.js new file mode 100644 index 00000000..d5f04f1c --- /dev/null +++ b/packages/my-joy-images/src/state/validators.js @@ -0,0 +1,67 @@ +import intercept from 'apr-intercept'; +import keys from 'lodash.keys'; +import reduce from 'apr-reduce'; +import assign from 'lodash.assign'; +import yup from 'yup'; + +/*****************************************************************************/ + +const validateField = async (field, value) => { + const [err] = await intercept(field.validate(value)); + return err ? err.errors.shift() : ''; +}; + +const validateSchema = async (schema, value) => { + const errors = await reduce( + keys(schema), + async (errors, name) => + assign(errors, { + [name]: await validateField(schema[name], value[name]) + }), + {} + ); + + throw errors; +}; + +/*****************************************************************************/ + +const matches = { + nameStart: /^[a-zA-Z]|\d/, + nameBody: /^([a-zA-Z]|\d|\.|_|-)+$/ +}; + +const msgs = { + required: prefix => `${prefix} must be defined.`, + nameStart: prefix => `${prefix} can only start with letters and numbers.`, + nameBody: prefix => + `${prefix} cannot contain spaces and can only contain letters, numbers, periods (.), underscores (_), and hyphens (-).` +}; + +const Schemas = { + tag: { + name: yup + .string() + .required(msgs.required('Key')) + .matches(matches.nameStart, msgs.nameStart('Key')) + .matches(matches.nameBody, msgs.nameBody('Key')), + value: yup + .string() + .required(msgs.required('Value')) + .matches(matches.nameStart, msgs.nameStart('Value')) + .matches(matches.nameBody, msgs.nameBody('Value')) + }, + instanceName: { + name: yup + .string() + .matches(matches.nameStart, msgs.nameStart('Instance name')) + .matches(matches.nameBody, msgs.nameBody('Instance Name')) + } +}; + +/*****************************************************************************/ + +export const addTag = tag => validateSchema(Schemas.tag, tag); + +export const instanceName = ({ name }) => + !name ? null : validateSchema(Schemas.instanceName, { name }); diff --git a/packages/my-joy-instances/package.json b/packages/my-joy-instances/package.json index 96bd9647..e3f39a12 100644 --- a/packages/my-joy-instances/package.json +++ b/packages/my-joy-instances/package.json @@ -11,7 +11,7 @@ "build:lib": "echo 0", "build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr", "prepublish": "NODE_ENV=production redrun build:bundle", - "lint": "redrun lint:ci -- -- --fix", + "lint": "redrun lint:ci -- --fix", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md", "test": "DEFAULT_TIMEOUT_INTERVAL=100000 NODE_ENV=test joyent-react-scripts test --env=jsdom", "test:ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'", @@ -22,6 +22,7 @@ "@manaflair/redux-batch": "^0.1.0", "apollo": "^0.2.2", "apr-intercept": "^3.0.3", + "apr-reduce": "^3.0.3", "bytes": "^3.0.0", "clipboard-copy": "^1.4.2", "constant-case": "^2.0.0", @@ -47,13 +48,13 @@ "lodash.isfunction": "^3.0.9", "lodash.isinteger": "^4.0.4", "lodash.omit": "^4.5.0", + "lodash.reduce": "^4.6.0", "lodash.reverse": "^4.0.1", "lodash.some": "^4.6.0", "lodash.sortby": "^4.7.0", "lodash.uniqby": "^4.7.0", "lodash.values": "^4.3.0", "param-case": "^2.1.1", - "punycode": "^2.1.0", "query-string": "^5.1.0", "react": "^16.2.0", "react-apollo": "^2.0.4", @@ -68,7 +69,8 @@ "styled-components": "^3.1.6", "styled-components-spacing": "^2.1.3", "styled-flex-component": "^2.2.1", - "title-case": "^2.1.1" + "title-case": "^2.1.1", + "yup": "^0.24.1" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/packages/my-joy-instances/src/components/__tests__/__image_snapshots__/cns-ui-js-add-service-form-pristine-1-snap.png b/packages/my-joy-instances/src/components/__tests__/__image_snapshots__/cns-ui-js-add-service-form-pristine-1-snap.png index f11153e3..e7bb037d 100644 Binary files a/packages/my-joy-instances/src/components/__tests__/__image_snapshots__/cns-ui-js-add-service-form-pristine-1-snap.png and b/packages/my-joy-instances/src/components/__tests__/__image_snapshots__/cns-ui-js-add-service-form-pristine-1-snap.png differ diff --git a/packages/my-joy-instances/src/components/__tests__/__snapshots__/cns.spec.js.snap b/packages/my-joy-instances/src/components/__tests__/__snapshots__/cns.spec.js.snap index e499a16f..99f50f59 100644 --- a/packages/my-joy-instances/src/components/__tests__/__snapshots__/cns.spec.js.snap +++ b/packages/my-joy-instances/src/components/__tests__/__snapshots__/cns.spec.js.snap @@ -466,11 +466,6 @@ exports[`renders without throwing 1`] = ` background-color: rgb(59,70,204); border-radius: 0.25rem; border: solid 0.0625rem rgb(45,56,132); - cursor: not-allowed; - pointer-events: none; - color: rgb(216,216,216); - background-color: rgb(250,250,250); - border-color: rgb(216,216,216); } .c7:focus { @@ -500,23 +495,6 @@ exports[`renders without throwing 1`] = ` pointer-events: none; } -.c7:focus { - background-color: rgb(250,250,250); - border-color: rgb(216,216,216); -} - -.c7:hover { - background-color: rgb(250,250,250); - border-color: rgb(250,250,250); -} - -.c7:active, -.c7:active:hover, -.c7:active:focus { - background-color: rgb(250,250,250); - border-color: rgb(250,250,250); -} - .c3 { font-size: 0.9375rem; line-height: 1.125rem; @@ -678,7 +656,7 @@ exports[`renders without throwing 1`] = ` >