Skip to content

Commit

Permalink
feat: create dataset style, tooltips, text
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswhong committed Sep 10, 2019
1 parent 6f6cd13 commit 80e0518
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 27 deletions.
32 changes: 29 additions & 3 deletions app/components/form/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'

export interface TextInputProps {
label?: string
labelTooltip?: string
name: string
type: string
value: any
Expand All @@ -15,16 +18,39 @@ export interface TextInputProps {
white?: boolean
}

const TextInput: React.FunctionComponent<TextInputProps> = ({ label, name, type, value, maxLength, errorText, helpText, showHelpText, onChange, onKeyDown, placeHolder
}) => {
const TextInput: React.FunctionComponent<TextInputProps> = (props) => {
const {
label,
labelTooltip,
name,
type,
value,
maxLength,
errorText,
helpText,
showHelpText,
onChange,
onKeyDown,
placeHolder
} = props

const feedbackColor = errorText ? 'error' : showHelpText && helpText ? 'textMuted' : ''
const feedback = errorText || (showHelpText &&
helpText)
const labelColor = 'primary'
return (
<>
<div className='text-input-container'>
{label && <span className={labelColor}>{label}</span>}
{label && <><span className={labelColor}>{label}</span>&nbsp;&nbsp;</>}
{labelTooltip && (
<span
data-tip={labelTooltip}
data-for={'modal-tooltip'}
className='text-input-tooltip'
>
<FontAwesomeIcon icon={faInfoCircle} size='sm'/>
</span>
)}
<input
id={name}
name={name}
Expand Down
15 changes: 11 additions & 4 deletions app/components/modals/CreateDataset.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from 'react'
import { remote } from 'electron'

import Modal from './Modal'
import ExternalLink from '../ExternalLink'
import { ApiAction } from '../../store/api'
import TextInput from '../form/TextInput'
import Error from './Error'
Expand Down Expand Up @@ -147,12 +149,15 @@ const CreateDataset: React.FunctionComponent<CreateDatasetProps> = ({ onDismisse
dismissable={dismissable}
setDismissable={setDismissable}
>
<div className='content-wrap'>
<div className='content-wrap' >
<div className='content'>
<p>Qri will create a directory for your new dataset, containing files linked to each of the dataset&apos;s <ExternalLink href='https://qri.io/docs/concepts/dataset/'>components</ExternalLink>.</p>
<p>The data file you specify will become your new dataset&apos;s <ExternalLink href='https://qri.io/docs/reference/dataset/#body'>body</ExternalLink> component.</p>
<div className='flex-space-between'>
<TextInput
name='path'
label='Data file'
label='Source data file'
labelTooltip='Select a CSV or JSON file on your file system.<br/>Qri will import the data and leave the file in place.'
type=''
value={filePath}
onChange={handleChanges}
Expand All @@ -163,7 +168,8 @@ const CreateDataset: React.FunctionComponent<CreateDatasetProps> = ({ onDismisse
</div>
<TextInput
name='datasetName'
label='Dataset Name'
label='Name'
labelTooltip='Name will be the primary<br/> way to refer to your dataset.'
type=''
value={datasetName}
onChange={handleChanges}
Expand All @@ -173,7 +179,8 @@ const CreateDataset: React.FunctionComponent<CreateDatasetProps> = ({ onDismisse
<div className='flex-space-between'>
<TextInput
name='path'
label='Save Path'
label='Directory path'
labelTooltip='Qri will create a new directory for<br/>this dataset&apos;s files at this location.'
type=''
value={path}
onChange={handleChanges}
Expand Down
14 changes: 14 additions & 0 deletions app/components/modals/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react'
import classNames from 'classnames'
import ModalHeader from './header'
import ReactTooltip from 'react-tooltip'

/**
* Title bar height in pixels. Values taken from 'app/styles/_variables.scss'.
Expand Down Expand Up @@ -224,6 +225,11 @@ const Modal: React.FunctionComponent<ModalProps> = ({ title, dismissable = false
className
)

// we are rendering a second instance of ReactTooltip here because the modal
// is displayed in a <dialog> element, which always wants to live above the
// rest of the page. To add tooltips in a modal, add data-tip='string' and
// data-for='modal-tooltip' (the id of the ReactTooltip instance)

return (
<dialog
open={false}
Expand All @@ -238,6 +244,14 @@ const Modal: React.FunctionComponent<ModalProps> = ({ title, dismissable = false
{children}
</fieldset>
</form>
<ReactTooltip
id='modal-tooltip'
place='top'
type='dark'
effect='solid'
delayShow={500}
multiline
/>
</dialog>
)
}
Expand Down
4 changes: 2 additions & 2 deletions app/scss/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,11 @@ button.input {
-o-transition: all .2s ease-in;
-webkit-transition: all .2s ease-in;
transition: all .2s ease-in;
color: $primary-muted;
color: $primary;
border: none;
cursor: pointer;
&:hover {
color: $primary;
color: $primary-dark;
}
}

Expand Down
8 changes: 8 additions & 0 deletions app/scss/_welcome.scss
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,11 @@
.text-input-container {
width: 100%
}

.text-input-tooltip {
svg {
path {
fill: #c1c1c1;
}
}
}
22 changes: 16 additions & 6 deletions app/utils/formValidation.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
type ValidationError = string | null

export const ERR_INVALID_USERNAME_CHARACTERS: ValidationError = 'Usernames may only include a-z, 0-9, _ , and -'
export const ERR_INVALID_USERNAME_LENGTH: ValidationError = 'Username must be 50 characters or fewer'

// validators return an error message or null
export const validateUsername = (username: string): ValidationError => {
if (username) {
const invalidCharacters = !(/^[a-z0-9_-]+$/.test(username))
if (invalidCharacters) return 'Usernames may only include a-z, 0-9, _ , and -'
if (invalidCharacters) return ERR_INVALID_USERNAME_CHARACTERS

const tooLong = username.length > 50
if (tooLong) return 'Username must be 50 characters or less'
if (tooLong) return ERR_INVALID_USERNAME_LENGTH
}
return null
}

export const ERR_INVALID_EMAIL: ValidationError = 'Enter a valid email address'

export const validateEmail = (email: string): ValidationError => {
if (email) {
const invalidEmail = !(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
if (invalidEmail) return 'Enter a valid email address'
if (invalidEmail) return ERR_INVALID_EMAIL
}
return null
}

export const ERR_INVALID_PASSWORD_LENGTH: ValidationError = 'Password must be at least 8 characters'

export const validatePassword = (password: string) => {
if (password) {
const tooShort = password && password.length < 8
if (tooShort) return 'Password must be at least 8 characters'
if (tooShort) return ERR_INVALID_PASSWORD_LENGTH
}
return null
}

export const ERR_INVALID_DATASETNAME_CHARACTERS: ValidationError = 'Dataset names may only include a-z, 0-9, and _'
export const ERR_INVALID_DATASETNAME_LENGTH: ValidationError = 'Username must be 100 characters or fewer'

export const validateDatasetName = (name: string): ValidationError => {
if (name) {
const invalidCharacters = !(/^[a-z0-9_]+$/.test(name))
if (invalidCharacters) return 'Dataset names may only include a-z, 0-9, and _'
if (invalidCharacters) return ERR_INVALID_DATASETNAME_CHARACTERS

const tooLong = name.length > 100
if (tooLong) return 'Username must be 100 characters or fewer'
if (tooLong) return ERR_INVALID_DATASETNAME_LENGTH
}
return null
}
139 changes: 139 additions & 0 deletions test/app/utils/formValidation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
validateUsername,
validateEmail,
validatePassword,
validateDatasetName,
ERR_INVALID_USERNAME_CHARACTERS,
ERR_INVALID_USERNAME_LENGTH,
ERR_INVALID_EMAIL,
ERR_INVALID_PASSWORD_LENGTH,
ERR_INVALID_DATASETNAME_CHARACTERS,
ERR_INVALID_DATASETNAME_LENGTH
} from '../../../app/utils/formValidation'

describe('formValidation', () => {
const usernameGoodCases = [
'foo_bar',
'foo27',
'foo-bar',
'sp4vsihuof65kgyhcmv0agbjgcwljfufkjidk5wmqhl0mxg61n'
]

usernameGoodCases.forEach((string) => {
it(`validateUsername accepts ${string}`, () => {
const got = validateUsername(string)
expect(got).toBe(null)
})
})

const userNameBadCases = [
{
string: 'foo👋bar',
err: ERR_INVALID_USERNAME_CHARACTERS
},
{
string: 'iceman@34',
err: ERR_INVALID_USERNAME_CHARACTERS
},
{
string: 'sp4vsihuof65kgyhcmv0agbjgcwljfufkjidk5wmqhl0mxg61n182',
err: ERR_INVALID_USERNAME_LENGTH
}
]

userNameBadCases.forEach(({ string, err }) => {
it(`validateUsername rejects ${string}`, () => {
const got = validateUsername(string)
expect(got).toBe(err)
})
})

const emailGoodCases = [
'foo@bar.com'
]

emailGoodCases.forEach((string) => {
it(`validateEmail accepts ${string}`, () => {
const got = validateEmail(string)
expect(got).toBe(null)
})
})

const emailBadCases = [
{
string: 'foobar',
err: ERR_INVALID_EMAIL
},
{
string: 'foo+bar',
err: ERR_INVALID_EMAIL
}
]

emailBadCases.forEach(({ string, err }) => {
it(`validateEmail rejects ${string}`, () => {
const got = validateEmail(string)
expect(got).toBe(err)
})
})

const passwordGoodCases = [
'someLongPassword'
]

passwordGoodCases.forEach((string) => {
it(`validatePassword accepts ${string}`, () => {
const got = validatePassword(string)
expect(got).toBe(null)
})
})

const passwordBadCases = [
{
string: 'imShort',
err: ERR_INVALID_PASSWORD_LENGTH
}
]

passwordBadCases.forEach(({ string, err }) => {
it(`validatePassword rejects ${string}`, () => {
const got = validatePassword(string)
expect(got).toBe(err)
})
})

const datasetNameGoodCases = [
'a',
'hello_world',
'pmfqbx5bhe7w4nbonqj6zu2abb15txq7vc5yfgysawjbdiqaxghvt4iy3rdyhvxg2v52mcsqeh1yymxe6ciz1lsxwmfsqyzdkh'
]

datasetNameGoodCases.forEach((string) => {
it(`validateDatasetName accepts ${string}`, () => {
const got = validateDatasetName(string)
expect(got).toBe(null)
})
})

const datasetNameBadCases = [
{
string: 'hello-world',
err: ERR_INVALID_DATASETNAME_CHARACTERS
},
{
string: '👋🏻',
err: ERR_INVALID_DATASETNAME_CHARACTERS
},
{
string: 'pmfqbx5bhe7w4nbonqj6zu2abb15txq7vc5yfgysawjbdiqaxghvt4iy3rdyhvxg2v52mcsqeh1yymxe6ciz1lsxwmfsqyzdkhsdf3182',
err: ERR_INVALID_DATASETNAME_LENGTH
}
]

datasetNameBadCases.forEach(({ string, err }) => {
it(`validateDatasetName rejects ${string}`, () => {
const got = validateDatasetName(string)
expect(got).toBe(err)
})
})
})
12 changes: 6 additions & 6 deletions test/runTests.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const spawn = require('cross-spawn');
const path = require('path');
const spawn = require('cross-spawn')
const path = require('path')

const s = `\\${path.sep}`;
const s = `\\${path.sep}`
const pattern = process.argv[2] === 'e2e'
? `test${s}e2e${s}.+\\.spec\\.tsx?`
: `test${s}(?!e2e${s})[^${s}]+${s}.+\\.spec\\.tsx?$`;
: `test${s}(?!e2e${s})[^${s}]+${s}.+\\.test\\.ts?$`

const result = spawn.sync(path.normalize('./node_modules/.bin/jest'), [pattern], { stdio: 'inherit' });
const result = spawn.sync(path.normalize('./node_modules/.bin/jest'), [pattern], { stdio: 'inherit' })

process.exit(result.status);
process.exit(result.status)
Loading

0 comments on commit 80e0518

Please sign in to comment.