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

feat(Schema): create a schema editor #392

Merged
merged 8 commits into from
Jan 17, 2020
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