Skip to content

Commit

Permalink
feat(Schema): create a schema editor (#392)
Browse files Browse the repository at this point in the history
* feat(components): add components that build to create TypePicker

added components:
- overlay/Overlay: used as a pop over, will call `onCancel` if you click outside fo the div, or click the 'x'. The `navigation` prop is optional. Any `children` will overflow-y scroll.
- item/DataType.tsx: defines the `DataTypes` type. Wraps the type name, description, and icon. `type` and `description`, are defined and passed down. May want to refactor to keep description and type defined here, and just use the type to determine all content.
- nav/TabPicker: has size options 'sm' and 'md', and color options 'light' and 'dark'
- overlay/TypePickerOverlay: a version of `Overlay` that lists the DataTypes and passes through the `navigation` component. `typesAndDescriptions` are defined here.
- structure/ColumnType: the component used to show the column type with its icon. Is not 'clickable' if there is no 'onClick' func passed down. I a column has more than one type, it will indicate 'multi'
- structure/TypePicker: takes all these components and creates a TypePicker. Click on the columnType to get a TypePickerOverlay popup. Choose between single or multi mode. The chosen type is kept in the hidden input named for the column (`${column-name}-type`)

This commit also created scss files based on the folder structure:
- chrome
- item
- nav
- overlay
- structure
- type

It also adds a story for each component under the `structure` tab

* feat(DynamicEditField): in place input field w/ validation

modeled after the `DatasetReference` component
Ran into a bizarre issue where the `commitEdit` function was only ever pulling from the initial state values, rather then the current state. Had to do a weird hack where I used the `prev` value in the setState functions to get the actual state...

Added a new story for `Form` items

* feat(SchemaItem): row that contains editable schema info

- adjusts ColumnType, TypePicker, and DynamicEditField to have `expanded` view
- adds up, down, left, right carets to `Icon`
- creates `SchemaItem` that expands and retracts
- known bug, DynamicEditField doesn't expand fully at first when you expand SchemaItem

* feat(Schema): add schema, adjust DynamicEditField to use div

DynamicEditField uses editable `div` rather then `input`

* refactor(DynamicEditField): adjust where we pull value now that we are no longer using `input`

* refactor(Schema): add non-editable state and bring Schema into app

`Schema` is now apart of the `Structure` component! We have schema editing in app :)

* refactor(Schema): handle strange types

make adjustments to the way we handle unknown types and 'any'

* refactor(structure): adjust inputs and function signatures
internally we have decided to pass all the 'stuff' that a component needs to display as the prop `data`, and that each component should handle their own events
this pr required refactoring to match with this paradigm
  • Loading branch information
ramfox authored Jan 17, 2020
1 parent 670be1a commit 4d9b4f1
Show file tree
Hide file tree
Showing 16 changed files with 750 additions and 99 deletions.
33 changes: 0 additions & 33 deletions app/components/Schema.tsx

This file was deleted.

21 changes: 15 additions & 6 deletions app/components/Structure.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react'
import Schema from './Schema'
import { Structure as IStructure } from '../models/dataset'
import Schema from './structure/Schema'
import { Structure as IStructure, Schema as ISchema } from '../models/dataset'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import ExternalLink from './ExternalLink'
Expand Down Expand Up @@ -61,7 +61,7 @@ export const formatConfigOptions: { [key: string]: FormatConfigOption } = {

const Structure: React.FunctionComponent<StructureProps> = ({ peername, name, structure, history, write, fsiBodyFormat }) => {
const format = history ? structure.format : fsiBodyFormat
const handleWrite = (option: string, value: any) => {
const handleWriteFormat = (option: string, value: any) => {
// TODO (ramfox): sending over format since a user can replace the body with a body of a different
// format. Let's pass in whatever the current format is, so that we have unity between
// what the desktop is seeing and the backend. This can be removed when we have the fsi
Expand All @@ -77,21 +77,26 @@ const Structure: React.FunctionComponent<StructureProps> = ({ peername, name, st
}
})
}

const handleOnChange = (schema: ISchema) => {
write(peername, name, { structure: { ...structure, schema } })
}

return (
<div className='structure content'>
{ history
? <FormatConfigHistory structure={structure} />
: <FormatConfigFSI
structure={structure}
format={format}
write={handleWrite}
write={handleWriteFormat}
/>
}
<div>
<h4 className='schema-title'>
Schema
&nbsp;
<ExternalLink href='https://json-schema.org/'>
<ExternalLink id='json-schema' href='https://json-schema.org/'>
<span
data-tip={'JSON schema that describes the structure of the dataset. Click here to learn more about JSON schemas'}
className='text-input-tooltip'
Expand All @@ -100,7 +105,11 @@ const Structure: React.FunctionComponent<StructureProps> = ({ peername, name, st
</span>
</ExternalLink>
</h4>
<Schema schema={structure ? structure.schema : undefined} />
<Schema
data={structure ? structure.schema : undefined}
onChange={handleOnChange}
editable={!history}
/>
</div>
</div>
)
Expand Down
12 changes: 10 additions & 2 deletions app/components/chrome/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
faFileAlt,
faCopy,
faTimes,
faCheck
faCheck,
faCaretLeft,
faCaretRight,
faCaretUp,
faCaretDown
} from '@fortawesome/free-solid-svg-icons'

interface IconProps {
Expand Down Expand Up @@ -62,7 +66,11 @@ const icons: {[key: string]: any} = {
'lock': faLock,
'transform': faCode,
'close': faTimes,
'check': faCheck
'check': faCheck,
'left': faCaretLeft,
'right': faCaretRight,
'up': faCaretUp,
'down': faCaretDown
}

export const iconsList = Object.keys(icons)
Expand Down
151 changes: 151 additions & 0 deletions app/components/form/DynamicEditField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as React from 'react'
import classNames from 'classnames'

interface DynamicEditFieldProps {
placeholder?: string
// validate function expects a value string and returns a boolean
// called when user changes input value
// true means the input is valid, false means it's invalid
validate?: (value: string) => boolean
onChange?: (name: string, value: string, e?: React.SyntheticEvent) => void
value: string
allowEmpty: boolean
width?: number
maxLength?: number
expanded?: boolean
name: string
row?: number
large?: boolean
// editable defaults to true
editable?: boolean
}

const DynamicEditField: React.FunctionComponent<DynamicEditFieldProps> = ({
placeholder = '',
value,
name,
validate,
onChange,
allowEmpty = true,
width,
maxLength,
expanded = false,
row = 0,
large = false,
editable = true
}) => {
const [ newValue, setNewValue ] = React.useState(value)
const [ isValid, setIsValid ] = React.useState(true)

const commitEdit = (e: React.SyntheticEvent) => {
// TODO (ramfox): for some reason, the only way I can get the actual updated
// state value is by hacking into the `setNewValue` function, which passes
// the previous value into a function
// wtf do i have to do this?!?
let newValueHack = newValue
setNewValue((prev) => {
newValueHack = prev
return prev
})
let isValidHack = false
setIsValid((prev) => {
isValidHack = prev
return prev
})
if ((value === newValueHack) || !isValidHack || (!allowEmpty && newValueHack === '')) {
cancelEdit()
} else if (onChange) {
onChange(name, newValueHack, e)
}
// drop focus
ref.current.blur()
}

const cancelEdit = () => {
setNewValue(value)
ref.current.innerHTML = value
setIsValid(true)
ref.current.blur()
}

const handleKeyDown = (e: React.KeyboardEvent) => {
// cancel on esc
if (e.keyCode === 27) {
cancelEdit()
}

// submit on enter or tab
if ((e.keyCode === 13) || (e.keyCode === 9)) {
commitEdit(e)
}
}

// use a ref so we can set up a click handler
const ref: any = React.useRef()

const handleMousedown = (e: React.MouseEvent) => {
const { target } = e
// allows the user to resize the sidebar when editing the dataset name
if (target.classList.contains('resize-handle')) return

if (!ref.current.contains(target)) {
commitEdit(e)
}
}

const [focused, setFocused] = React.useState(false)

// only add event listeners when we are focused
React.useEffect(() => {
if (focused) {
document.addEventListener('keydown', handleKeyDown, false)
document.addEventListener('mousedown', handleMousedown, false)
} else {
document.removeEventListener('keydown', handleKeyDown, false)
document.removeEventListener('mousedown', handleMousedown, false)
}
return () => {
document.removeEventListener('keydown', handleKeyDown, false)
document.removeEventListener('mousedown', handleMousedown, false)
}
}, [focused])

const handleChange = async (e: any) => {
let value = e.target.innerHTML
if (maxLength && value.length > maxLength) {
return
}
if (validate) {
setIsValid(validate(value))
}
setNewValue(value)
}

const onFocus = () => {
setFocused(true)
ref.current.scrollLeft = 0
}

const onBlur = () => {
setFocused(false)
ref.current.scrollLeft = 0
}

return (
<div style={{ width }} className={classNames('dynamic-edit-field', { 'invalid': !isValid, 'dynamic-edit-field-large': large, 'focused': focused, 'dynamic-edit-field-editable': editable })} >
<div
suppressContentEditableWarning={true}
className={classNames({ 'expanded': expanded })}
contentEditable={editable}
onInput={handleChange}
ref={ref}
id={`${name}-${row}`}
data-placeholder={placeholder}
onFocus={onFocus}
onBlur={onBlur}
>{value}</div>
</div>
)
}

export default DynamicEditField
110 changes: 110 additions & 0 deletions app/components/item/SchemaItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react'
import Icon from '../chrome/Icon'
import DynamicEditField from '../form/DynamicEditField'
import { DataTypes } from './DataType'
import TypePicker from '../structure/TypePicker'
import classNames from 'classnames'

interface SchemaItemProps {
onChange?: (schemaItem: SchemaItemType, e: React.SyntheticEvent) => void
// editable defaults to true
editable?: boolean
data: SchemaItemType
}

export interface SchemaItemType {
row: number
title: string
type: DataTypes | DataTypes[]
description: string
validation: string
}

const SchemaItemProps: React.FunctionComponent<SchemaItemProps> = ({
onChange,
data,
editable = true
}) => {
const [expanded, setExpanded] = React.useState(false)

const handleDynamicEditChange = (name: string, value: string, e: React.SyntheticEvent) => {
const d = { ...data }
switch (name) {
case 'title':
d.title = value
break
case 'description':
d.description = value
break
case 'validation':
d.validation = value
break
}

if (onChange) onChange(d, e)
}

const handleTypePickerChange = (value: DataTypes, e: React.SyntheticEvent) => {
const d = { ...data }
d.type = value
if (onChange) onChange(d, e)
}

// TODO (ramfox): do we have max lengths for title, description?
return (
<div className={classNames('schema-item', { 'expanded': expanded, 'top': data.row === 0 })} key={data.row}>
<div className='schema-item-icon' onClick={() => setExpanded((prev) => !prev)} >
<Icon icon={expanded ? 'down' : 'right'} size='md' color='medium'/>
</div>
<div>
<DynamicEditField
row={data.row}
name='title'
placeholder='title'
value={data.title || ''}
onChange={onChange && editable ? handleDynamicEditChange : undefined}
allowEmpty={false}
large
width={150}
expanded={expanded}
editable={editable}
/>
</div>
<div>
<TypePicker
name={data.row}
onPickType={onChange && editable ? handleTypePickerChange : undefined}
type={data.type}
expanded={expanded}
editable={editable}
/>
</div>
<div>
<DynamicEditField
row={data.row}
name='description'
placeholder='description'
value={data.description || ''}
onChange={onChange && editable ? handleDynamicEditChange : undefined}
allowEmpty expanded={expanded}
width={200}
editable={editable}
/>
</div>
<div>
<DynamicEditField
row={data.row}
name='validation'
placeholder='validation'
value={data.validation || ''}
onChange={onChange && editable ? handleDynamicEditChange : undefined}
allowEmpty expanded={expanded}
width={100}
editable={editable}
/>
</div>
</div>
)
}

export default SchemaItemProps
Loading

0 comments on commit 4d9b4f1

Please sign in to comment.