smallstonesk
4 years ago
29 changed files with 0 additions and 2492 deletions
@ -1,3 +0,0 @@ |
|||
{ |
|||
"extends": "../../.babelrc" |
|||
} |
@ -1,2 +0,0 @@ |
|||
node_modules |
|||
src |
@ -1,11 +0,0 @@ |
|||
# `json-schema-editor` |
|||
|
|||
> TODO: description |
|||
|
|||
## Usage |
|||
|
|||
``` |
|||
const schemaEditor = require('@imove/json-schema-editor'); |
|||
|
|||
// TODO: DEMONSTRATE API |
|||
``` |
File diff suppressed because it is too large
@ -1,66 +0,0 @@ |
|||
{ |
|||
"name": "@imove/json-schema-editor", |
|||
"version": "0.3.2", |
|||
"description": "visual editor for json schema", |
|||
"keywords": [ |
|||
"ace-editor", |
|||
"json-schema" |
|||
], |
|||
"author": "suanmei <mr_suanmei@163.com>", |
|||
"homepage": "https://github.com/suanmei/iMove#readme", |
|||
"license": "MIT", |
|||
"main": "dist/core.common.js", |
|||
"module": "dist/core.esm.js", |
|||
"types": "dist/types/index.d.ts", |
|||
"directories": { |
|||
"dist": "dist" |
|||
}, |
|||
"files": [ |
|||
"dist" |
|||
], |
|||
"publishConfig": { |
|||
"access": "public", |
|||
"registry": "http://registry.npmjs.org/" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "git+https://github.com/suanmei/iMove.git" |
|||
}, |
|||
"scripts": { |
|||
"prepublishOnly": "node scripts/prepublish.js", |
|||
"declare-type": "tsc --emitDeclarationOnly", |
|||
"build": "rollup -c & npm run declare-type", |
|||
"watch": "watch \"npm run build\" ./src" |
|||
}, |
|||
"bugs": { |
|||
"url": "https://github.com/suanmei/iMove/issues" |
|||
}, |
|||
"devDependencies": { |
|||
"@ant-design/icons": "^4.1.0", |
|||
"@emotion/core": "^10.0.28", |
|||
"@emotion/styled": "^10.0.27", |
|||
"@types/lodash-es": "^4.17.3", |
|||
"@types/react": "^16.9.34", |
|||
"@types/react-dom": "^16.9.7", |
|||
"antd": "^4.1.5", |
|||
"react": "^16.13.1", |
|||
"rollup": "^2.7.3", |
|||
"watch": "^1.0.2" |
|||
}, |
|||
"peerDependencies": { |
|||
"@ant-design/icons": "^4.1.0", |
|||
"@emotion/core": "^10.0.28", |
|||
"@emotion/styled": "^10.0.27", |
|||
"antd": "^4.1.5", |
|||
"react": "^16.13.1", |
|||
"react-dom": "^16.13.1" |
|||
}, |
|||
"gitHead": "60b114ff262513544204f33a9bbcb6220d6c1c5d", |
|||
"dependencies": { |
|||
"i18next": "^19.4.4", |
|||
"immer": "^8.0.1", |
|||
"lodash-es": "^4.17.15", |
|||
"react-i18next": "^11.4.0", |
|||
"react-tracked": "^1.3.0" |
|||
} |
|||
} |
@ -1,16 +0,0 @@ |
|||
import rollupBaseConfig from '../../rollup.config'; |
|||
import pkg from './package.json'; |
|||
|
|||
export default Object.assign(rollupBaseConfig, { |
|||
output: [ |
|||
{ |
|||
file: pkg.main, |
|||
format: 'cjs', |
|||
}, |
|||
{ |
|||
file: pkg.module, |
|||
format: 'es', |
|||
}, |
|||
], |
|||
external: Object.keys(pkg.peerDependencies).concat(/^antd\//), |
|||
}); |
@ -1,6 +0,0 @@ |
|||
#!/usr/bin/env node
|
|||
|
|||
const path = require('path'); |
|||
const prePublish = require('../../../scripts/prepublish'); |
|||
|
|||
prePublish('@imove/json-schema-editor', path.join(__dirname, '../')); |
@ -1,48 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import { Menu, Dropdown } from 'antd'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { PlusIcon } from '../utils/styles/common'; |
|||
|
|||
interface AddNodeProps { |
|||
addChildField: () => void; |
|||
addSiblingField: () => void; |
|||
} |
|||
|
|||
function AddNodeIcon({ |
|||
addChildField, |
|||
addSiblingField, |
|||
}: AddNodeProps): JSX.Element { |
|||
const { t } = useTranslation(); |
|||
const menu = ( |
|||
<Menu> |
|||
<Menu.Item> |
|||
<div |
|||
tabIndex={0} |
|||
role="button" |
|||
onClick={addChildField} |
|||
onKeyDown={addChildField} |
|||
> |
|||
{t('child_node')} |
|||
</div> |
|||
</Menu.Item> |
|||
<Menu.Item> |
|||
<div |
|||
tabIndex={0} |
|||
role="button" |
|||
onClick={addSiblingField} |
|||
onKeyDown={addSiblingField} |
|||
> |
|||
{t('sibling_node')} |
|||
</div> |
|||
</Menu.Item> |
|||
</Menu> |
|||
); |
|||
|
|||
return ( |
|||
<Dropdown overlay={menu}> |
|||
<PlusIcon /> |
|||
</Dropdown> |
|||
); |
|||
} |
|||
|
|||
export default AddNodeIcon; |
@ -1,70 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import styled from '@emotion/styled'; |
|||
import { Tooltip, Input, Checkbox } from 'antd'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
|
|||
interface FieldInputProps { |
|||
value: string; |
|||
required: boolean; |
|||
changeField: (value: string) => void; |
|||
toggleRequired: (value: string) => void; |
|||
} |
|||
|
|||
const CustomInput = styled(Input)` |
|||
.ant-input-group-addon { |
|||
background: transparent; |
|||
border: none; |
|||
} |
|||
`;
|
|||
|
|||
function FieldInput(props: FieldInputProps): JSX.Element { |
|||
const { value: initValue, changeField, required, toggleRequired } = props; |
|||
const [value, setValue] = React.useState(initValue); |
|||
const [prevInitValue, setPrevInitValue] = React.useState(initValue); |
|||
|
|||
const { t } = useTranslation(); |
|||
|
|||
// mock getDerivedStateFromProps
|
|||
if (initValue !== prevInitValue) { |
|||
setValue(initValue); |
|||
setPrevInitValue(initValue); |
|||
} |
|||
|
|||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => { |
|||
setValue(e.target.value); |
|||
}; |
|||
|
|||
const handleBlur = (e: React.ChangeEvent<HTMLInputElement>): void => { |
|||
if (e.target.value !== initValue) { |
|||
changeField(e.target.value); |
|||
} |
|||
}; |
|||
|
|||
const handleKeyUp = (e: React.KeyboardEvent): void => { |
|||
const { value: newValue } = e.target as HTMLInputElement; |
|||
if (e.keyCode === 13 && newValue !== initValue) { |
|||
changeField(newValue); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<CustomInput |
|||
value={value} |
|||
onChange={handleChange} |
|||
onBlur={handleBlur} |
|||
onKeyUp={handleKeyUp} |
|||
addonAfter={ |
|||
<Tooltip placement="top" title={t('required')}> |
|||
<Checkbox |
|||
checked={required} |
|||
onChange={(): void => { |
|||
toggleRequired(value); |
|||
}} |
|||
/> |
|||
</Tooltip> |
|||
} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export default React.memo(FieldInput); |
@ -1,48 +0,0 @@ |
|||
/** @jsx jsx */ |
|||
import { useEffect } from 'react'; |
|||
import { jsx, css } from '@emotion/core'; |
|||
import { Button } from 'antd'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import EditorHead from './schema-head'; |
|||
import SchemaForm from './schema-form'; |
|||
import { useUIState } from '../store/ui'; |
|||
import { useSchemaState } from '../store/schema'; |
|||
import { SchemaState } from '../model'; |
|||
|
|||
let temp: SchemaState | null = null; |
|||
|
|||
interface LayoutProps { |
|||
onChange: (value: SchemaState) => void; |
|||
} |
|||
|
|||
function Layout({ onChange }: LayoutProps): JSX.Element { |
|||
const { t } = useTranslation(); |
|||
|
|||
const uiState = useUIState(); |
|||
const key = 'properties'; |
|||
const visible = uiState[key] !== false; |
|||
|
|||
const schemaState = useSchemaState(); |
|||
|
|||
useEffect(() => { |
|||
if (temp !== schemaState) { |
|||
temp = schemaState; |
|||
onChange(schemaState); |
|||
} |
|||
}); |
|||
|
|||
return ( |
|||
<div |
|||
className="json-schema-editor" |
|||
css={css` |
|||
padding: 5px; |
|||
`}
|
|||
> |
|||
<Button type="primary">{t('import_json')}</Button> |
|||
<EditorHead /> |
|||
{visible && <SchemaForm data={schemaState} keyRoute={['properties']} />} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default Layout; |
@ -1,44 +0,0 @@ |
|||
/** @jsx jsx */ |
|||
import { jsx } from '@emotion/core'; |
|||
import { isUndefined } from 'lodash-es'; |
|||
import SchemaItem from './schema-item'; |
|||
import { ObjectSchemaItem, KeyRoute } from '../model'; |
|||
|
|||
interface SchemaFormProps { |
|||
data: ObjectSchemaItem; |
|||
keyRoute: KeyRoute; |
|||
} |
|||
|
|||
function SchemaForm({ data, keyRoute }: SchemaFormProps): JSX.Element { |
|||
const { required, properties } = data; |
|||
|
|||
return ( |
|||
<div> |
|||
{Object.keys(properties).map((field, index) => { |
|||
const itemData = properties[field]; |
|||
const isRequired = required && required.includes(field); |
|||
const fieldKeyRoute = keyRoute.concat(field); |
|||
|
|||
return ( |
|||
<SchemaItem |
|||
// eslint-disable-next-line react/no-array-index-key
|
|||
key={index} |
|||
field={field} |
|||
data={itemData} |
|||
required={isRequired} |
|||
keyRoute={fieldKeyRoute} |
|||
> |
|||
{itemData.type === 'object' && !isUndefined(itemData.properties) ? ( |
|||
<SchemaForm |
|||
data={itemData} |
|||
keyRoute={fieldKeyRoute.concat('properties')} |
|||
/> |
|||
) : null} |
|||
</SchemaItem> |
|||
); |
|||
})} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default SchemaForm; |
@ -1,109 +0,0 @@ |
|||
/** @jsx jsx */ |
|||
import { useState } from 'react'; |
|||
import { jsx } from '@emotion/core'; |
|||
import styled from '@emotion/styled'; |
|||
import { Row, Col, Tooltip, Input, Checkbox, Select } from 'antd'; |
|||
import { CheckboxChangeEvent } from 'antd/lib/checkbox'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { useUIState, useSetUIState } from '../store/ui'; |
|||
import { useSchemaDispatch } from '../store/schema'; |
|||
import { |
|||
CaretDownIcon, |
|||
CaretRightIcon, |
|||
EditIcon, |
|||
SettingIcon, |
|||
PlusIcon, |
|||
} from '../utils/styles/common'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
const FieldInput = styled(Input)` |
|||
.ant-input-group-addon { |
|||
background: transparent; |
|||
border: none; |
|||
} |
|||
`;
|
|||
|
|||
function EditorHead(): JSX.Element { |
|||
const { t } = useTranslation(); |
|||
const schemaDispatch = useSchemaDispatch(); |
|||
const setUIState = useSetUIState(); |
|||
const uiState = useUIState(); |
|||
const key = 'properties'; |
|||
const visible = uiState[key]; |
|||
|
|||
const [checked, setChecked] = useState(false); |
|||
|
|||
const toggleFormVisible = (): void => { |
|||
setUIState((prev) => ({ ...prev, [key]: !visible })); |
|||
}; |
|||
|
|||
const addChildField = (): void => { |
|||
schemaDispatch({ type: 'ADD_CHILD_FIELD', keyRoute: ['properties'] }); |
|||
}; |
|||
|
|||
const toggleAllChecked = (e: CheckboxChangeEvent): void => { |
|||
setChecked(e.target.checked); |
|||
schemaDispatch({ type: 'TOGGLE_ALL_CHECKED', checked: e.target.checked }); |
|||
}; |
|||
|
|||
return ( |
|||
<Row css={{ marginTop: '10px' }}> |
|||
<Col span="24"> |
|||
<Row align="middle"> |
|||
<Col span="8"> |
|||
<Row align="middle"> |
|||
<Col span="2"> |
|||
{visible ? ( |
|||
<CaretDownIcon onClick={toggleFormVisible} /> |
|||
) : ( |
|||
<CaretRightIcon onClick={toggleFormVisible} /> |
|||
)} |
|||
</Col> |
|||
<Col span="22"> |
|||
<FieldInput |
|||
disabled |
|||
value="root" |
|||
addonAfter={ |
|||
<Tooltip placement="top" title={t('select_all')}> |
|||
<Checkbox checked={checked} onChange={toggleAllChecked} /> |
|||
</Tooltip> |
|||
} |
|||
/> |
|||
</Col> |
|||
</Row> |
|||
</Col> |
|||
<Col span="3"> |
|||
<Select value="object" disabled> |
|||
<Option value="object">object</Option> |
|||
</Select> |
|||
</Col> |
|||
<Col span="5"> |
|||
<Input |
|||
value="" |
|||
placeholder={t('title')} |
|||
addonAfter={<EditIcon />} |
|||
/> |
|||
</Col> |
|||
<Col span="5"> |
|||
<Input |
|||
value="" |
|||
placeholder={t('description')} |
|||
addonAfter={<EditIcon />} |
|||
/> |
|||
</Col> |
|||
<Col span="3"> |
|||
<Tooltip placement="top" title={t('setting')}> |
|||
<SettingIcon /> |
|||
</Tooltip> |
|||
<Tooltip placement="top" title={t('add_child_node')}> |
|||
<PlusIcon onClick={addChildField} /> |
|||
</Tooltip> |
|||
</Col> |
|||
</Row> |
|||
</Col> |
|||
</Row> |
|||
); |
|||
} |
|||
|
|||
export default EditorHead; |
@ -1,156 +0,0 @@ |
|||
/** @jsx jsx */ |
|||
import { jsx } from '@emotion/core'; |
|||
import { Row, Col, Tooltip, Input, Select } from 'antd'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { useUIState, useSetUIState } from '../store/ui'; |
|||
import { useSchemaDispatch } from '../store/schema'; |
|||
import { schemaType } from '../utils/schema'; |
|||
import { SchemaType, SchemaItem, KeyRoute } from '../model'; |
|||
import FieldInput from './field-input'; |
|||
import AddNode from './add-node'; |
|||
import { |
|||
CaretDownIcon, |
|||
CaretRightIcon, |
|||
EditIcon, |
|||
SettingIcon, |
|||
PlusIcon, |
|||
CloseIcon, |
|||
} from '../utils/styles/common'; |
|||
|
|||
const { Option } = Select; |
|||
|
|||
interface SchemaItemProps { |
|||
field: string; |
|||
data: SchemaItem; |
|||
required: boolean; |
|||
keyRoute: KeyRoute; |
|||
children: React.ReactChild | null; |
|||
} |
|||
|
|||
function SchemaFormItem(props: SchemaItemProps): JSX.Element { |
|||
const { field, data, required, keyRoute, children } = props; |
|||
const { t } = useTranslation(); |
|||
const schemaDispatch = useSchemaDispatch(); |
|||
const setUIState = useSetUIState(); |
|||
const uiState = useUIState(); |
|||
const key = keyRoute.join('-'); |
|||
const visible = uiState[key] !== false; |
|||
|
|||
const toggleFormVisible = (): void => { |
|||
setUIState((prev) => ({ ...prev, [key]: !visible })); |
|||
}; |
|||
|
|||
const toggleRequired = (value: string): void => { |
|||
const fieldKeyRoute = keyRoute.slice(0, -1).concat(value); |
|||
|
|||
if (required) { |
|||
schemaDispatch({ type: 'REMOVE_REQUIRED', keyRoute: fieldKeyRoute }); |
|||
} else { |
|||
schemaDispatch({ type: 'ADD_REQUIRED', keyRoute: fieldKeyRoute }); |
|||
} |
|||
}; |
|||
|
|||
const addChildField = (): void => { |
|||
schemaDispatch({ |
|||
type: 'ADD_CHILD_FIELD', |
|||
keyRoute: keyRoute.concat('properties'), |
|||
}); |
|||
}; |
|||
|
|||
const addSiblingField = (): void => { |
|||
schemaDispatch({ type: 'ADD_SIBLING_FIELD', keyRoute }); |
|||
}; |
|||
|
|||
const removeField = (): void => { |
|||
schemaDispatch({ type: 'REMOVE_FIELD', keyRoute }); |
|||
}; |
|||
|
|||
const changeField = (value: string): void => { |
|||
schemaDispatch({ type: 'CHANGE_FIELD', keyRoute, field: value }); |
|||
}; |
|||
|
|||
const changeType = (value: SchemaType): void => { |
|||
schemaDispatch({ type: 'CHANGE_TYPE', keyRoute, fieldType: value }); |
|||
}; |
|||
|
|||
const changeTitle = (e: React.ChangeEvent<HTMLInputElement>): void => { |
|||
schemaDispatch({ type: 'CHANGE_TITLE', keyRoute, title: e.target.value }); |
|||
}; |
|||
|
|||
const changeDesc = (e: React.ChangeEvent<HTMLInputElement>): void => { |
|||
schemaDispatch({ type: 'CHANGE_DESC', keyRoute, desc: e.target.value }); |
|||
}; |
|||
|
|||
return ( |
|||
<div css={{ marginTop: '10px' }}> |
|||
<Row align="middle"> |
|||
<Col span="8" css={{ paddingLeft: `${keyRoute.length * 10}px` }}> |
|||
<Row align="middle"> |
|||
<Col span="2"> |
|||
{data.type === 'object' && visible && ( |
|||
<CaretDownIcon onClick={toggleFormVisible} /> |
|||
)} |
|||
{data.type === 'object' && !visible && ( |
|||
<CaretRightIcon onClick={toggleFormVisible} /> |
|||
)} |
|||
</Col> |
|||
<Col span="22"> |
|||
<FieldInput |
|||
value={field} |
|||
required={required} |
|||
changeField={changeField} |
|||
toggleRequired={toggleRequired} |
|||
/> |
|||
</Col> |
|||
</Row> |
|||
</Col> |
|||
<Col span="3"> |
|||
<Select value={data.type} onChange={changeType}> |
|||
{schemaType.map((type) => ( |
|||
<Option key={type} value={type}> |
|||
{type} |
|||
</Option> |
|||
))} |
|||
</Select> |
|||
</Col> |
|||
<Col span="5"> |
|||
<Input |
|||
value={data.title} |
|||
onChange={changeTitle} |
|||
placeholder={t('title')} |
|||
addonAfter={<EditIcon />} |
|||
/> |
|||
</Col> |
|||
<Col span="5"> |
|||
<Input |
|||
value={data.description} |
|||
onChange={changeDesc} |
|||
placeholder={t('description')} |
|||
addonAfter={<EditIcon />} |
|||
/> |
|||
</Col> |
|||
<Col span="3"> |
|||
<Tooltip placement="top" title={t('setting')}> |
|||
<SettingIcon /> |
|||
</Tooltip> |
|||
<CloseIcon onClick={removeField} /> |
|||
{data.type === 'object' ? ( |
|||
<AddNode |
|||
addChildField={addChildField} |
|||
addSiblingField={addSiblingField} |
|||
/> |
|||
) : ( |
|||
<Tooltip placement="top" title={t('add_sibling_node')}> |
|||
<PlusIcon onClick={addSiblingField} /> |
|||
</Tooltip> |
|||
)} |
|||
</Col> |
|||
</Row> |
|||
{visible && children ? ( |
|||
<div css={{ marginTop: '10px' }}>{children}</div> |
|||
) : null} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default SchemaFormItem; |
@ -1,23 +0,0 @@ |
|||
import i18n from 'i18next'; |
|||
import { initReactI18next } from 'react-i18next'; |
|||
import * as en from './translations/en.json'; |
|||
import * as zh from './translations/zh.json'; |
|||
|
|||
i18n |
|||
.use(initReactI18next) // passes i18n down to react-i18next
|
|||
.init({ |
|||
fallbackLng: 'en', |
|||
resources: { |
|||
en, |
|||
zh, |
|||
'en-US': en, |
|||
'zh-CN': zh, |
|||
}, |
|||
lng: 'en', |
|||
keySeparator: false, // we do not use keys in form messages.welcome
|
|||
interpolation: { |
|||
escapeValue: false, // react already safes from xss
|
|||
}, |
|||
}); |
|||
|
|||
export default i18n; |
@ -1,14 +0,0 @@ |
|||
{ |
|||
"translation": { |
|||
"import_json": "Import JSON", |
|||
"select_all": "Select All", |
|||
"required": "required", |
|||
"title": "Title", |
|||
"description": "Description", |
|||
"setting": "Advanced Setting", |
|||
"child_node": "Child node", |
|||
"sibling_node": "Sibling node", |
|||
"add_child_node": "Add child node", |
|||
"add_sibling_node": "Add sibling node" |
|||
} |
|||
} |
@ -1,14 +0,0 @@ |
|||
{ |
|||
"translation": { |
|||
"import_json": "导入 JSON", |
|||
"select_all": "全选", |
|||
"required": "必选", |
|||
"title": "标题", |
|||
"description": "描述", |
|||
"setting": "高级设置", |
|||
"child_node": "子节点", |
|||
"sibling_node": "兄弟节点", |
|||
"add_child_node": "添加子节点", |
|||
"add_sibling_node": "添加兄弟节点" |
|||
} |
|||
} |
@ -1,37 +0,0 @@ |
|||
/** @jsx jsx */ |
|||
import { useEffect } from 'react'; |
|||
import { Global, jsx } from '@emotion/core'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import Layout from './components/layout'; |
|||
import { UIProvider } from './store/ui'; |
|||
import { SchemaProvider } from './store/schema'; |
|||
import globalStyles from './utils/styles/global'; |
|||
import { SchemaState } from './model'; |
|||
import './utils/styles/antd'; |
|||
import './i18n'; |
|||
|
|||
interface EditorProps { |
|||
lang?: string; |
|||
data: SchemaState; |
|||
onChange: (value: SchemaState) => void; |
|||
} |
|||
|
|||
function JsonSchemaEditor({ lang, data, onChange }: EditorProps): JSX.Element { |
|||
const { i18n } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (!lang) return; |
|||
i18n.changeLanguage(lang); |
|||
}, [i18n, lang]); |
|||
|
|||
return ( |
|||
<UIProvider> |
|||
<SchemaProvider initialData={data}> |
|||
<Global styles={globalStyles} /> |
|||
<Layout onChange={onChange} /> |
|||
</SchemaProvider> |
|||
</UIProvider> |
|||
); |
|||
} |
|||
|
|||
export default JsonSchemaEditor; |
@ -1,2 +0,0 @@ |
|||
export * from './ui'; |
|||
export * from './schema'; |
@ -1,51 +0,0 @@ |
|||
export type SchemaType = |
|||
| 'string' |
|||
| 'number' |
|||
| 'object' |
|||
| 'array' |
|||
| 'boolean' |
|||
| 'integer'; |
|||
|
|||
type basicSchemaType = 'string' | 'number' | 'boolean' | 'integer'; |
|||
|
|||
interface BasicSchemaItem { |
|||
title?: string; |
|||
description?: string; |
|||
type: basicSchemaType; |
|||
} |
|||
|
|||
export interface ObjectSchemaItem { |
|||
title?: string; |
|||
description?: string; |
|||
type: 'object'; |
|||
properties: { |
|||
[field: string]: SchemaItem; |
|||
}; |
|||
required: string[]; |
|||
} |
|||
|
|||
export interface ArraySchemaItem { |
|||
title?: string; |
|||
description?: string; |
|||
type: 'array'; |
|||
items: SchemaItem; |
|||
} |
|||
|
|||
export type SchemaItem = BasicSchemaItem | ObjectSchemaItem | ArraySchemaItem; |
|||
|
|||
export type SchemaState = ObjectSchemaItem; |
|||
|
|||
export type KeyRoute = (string | number)[]; |
|||
|
|||
export type SchemaAction = |
|||
| { type: 'SET_SCHEMA'; schema: SchemaState } |
|||
| { type: 'ADD_CHILD_FIELD'; keyRoute: KeyRoute } |
|||
| { type: 'ADD_SIBLING_FIELD'; keyRoute: KeyRoute } |
|||
| { type: 'REMOVE_FIELD'; keyRoute: KeyRoute } |
|||
| { type: 'TOGGLE_ALL_CHECKED'; checked: boolean } |
|||
| { type: 'ADD_REQUIRED'; keyRoute: KeyRoute } |
|||
| { type: 'REMOVE_REQUIRED'; keyRoute: KeyRoute } |
|||
| { type: 'CHANGE_FIELD'; keyRoute: KeyRoute; field: string } |
|||
| { type: 'CHANGE_TYPE'; keyRoute: KeyRoute; fieldType: SchemaType } |
|||
| { type: 'CHANGE_TITLE'; keyRoute: KeyRoute; title: string } |
|||
| { type: 'CHANGE_DESC'; keyRoute: KeyRoute; desc: string }; |
@ -1,3 +0,0 @@ |
|||
export interface UIState { |
|||
[key: string]: boolean; |
|||
} |
@ -1,197 +0,0 @@ |
|||
import produce, { Draft } from 'immer'; |
|||
import { get, set, unset, remove } from 'lodash-es'; |
|||
import { |
|||
SchemaType, |
|||
SchemaItem, |
|||
SchemaState, |
|||
SchemaAction, |
|||
KeyRoute, |
|||
} from '../model'; |
|||
import { defaultSchema, setAllFieldRequired } from '../utils/schema'; |
|||
|
|||
let fieldNum = 0; |
|||
|
|||
const setSchema = (schema: SchemaState): SchemaState => { |
|||
return schema; |
|||
}; |
|||
|
|||
const addRequired = (state: SchemaState, keyRoute: KeyRoute): void => { |
|||
const field = keyRoute.slice(-1)[0]; |
|||
const requiredKeyRoute = keyRoute.slice(0, -2).concat('required'); |
|||
const required = get(state, requiredKeyRoute) || []; |
|||
required.push(field); |
|||
set(state, requiredKeyRoute, required); |
|||
}; |
|||
|
|||
const removeRequired = (state: SchemaState, keyRoute: KeyRoute): void => { |
|||
const field = keyRoute.slice(-1)[0]; |
|||
const requiredKeyRoute = keyRoute.slice(0, -2).concat('required'); |
|||
const required = get(state, requiredKeyRoute) || []; |
|||
remove(required, (item) => item === field); |
|||
set(state, requiredKeyRoute, required); |
|||
}; |
|||
|
|||
const changeRequiredField = ( |
|||
state: SchemaState, |
|||
oldKeyRoute: KeyRoute, |
|||
newField: string, |
|||
): void => { |
|||
const oldField = oldKeyRoute.slice(-1)[0]; |
|||
const requiredKeyRoute = oldKeyRoute.slice(0, -2).concat('required'); |
|||
const required = get(state, requiredKeyRoute) || []; |
|||
const oldFieldIndex = required.indexOf(oldField); |
|||
|
|||
// new field extend the old field
|
|||
if (oldFieldIndex > -1) { |
|||
required.splice(oldFieldIndex, 1, newField); |
|||
set(state, requiredKeyRoute, required); |
|||
} |
|||
}; |
|||
|
|||
const toggleAllChecked = (state: SchemaState, checked: boolean): void => { |
|||
setAllFieldRequired(state, checked); |
|||
}; |
|||
|
|||
const addChildField = (state: Draft<SchemaState>, keyRoute: KeyRoute): void => { |
|||
fieldNum += 1; |
|||
|
|||
const newField = `field_${fieldNum}`; |
|||
|
|||
// add new field - string
|
|||
const newKeyRoute = keyRoute.concat(newField); |
|||
set(state, newKeyRoute, defaultSchema.string); |
|||
|
|||
// add new filed to its parent's required
|
|||
addRequired(state, newKeyRoute); |
|||
}; |
|||
|
|||
const addSiblingField = (state: SchemaState, keyRoute: KeyRoute): void => { |
|||
fieldNum += 1; |
|||
|
|||
const newField = `field_${fieldNum}`; |
|||
|
|||
// add new field - string
|
|||
const newKeyRoute = keyRoute.slice(0, -1).concat(newField); |
|||
set(state, newKeyRoute, defaultSchema.string); |
|||
|
|||
// add new filed to its parent's required
|
|||
addRequired(state, newKeyRoute); |
|||
}; |
|||
|
|||
const removeField = (state: SchemaState, keyRoute: KeyRoute): void => { |
|||
// remove field
|
|||
unset(state, keyRoute); |
|||
|
|||
// remove filed from its parent's required
|
|||
removeRequired(state, keyRoute); |
|||
}; |
|||
|
|||
const changeField = ( |
|||
state: SchemaState, |
|||
keyRoute: KeyRoute, |
|||
newField: string, |
|||
): void => { |
|||
const oldField = keyRoute.slice(-1)[0]; |
|||
const parentKeyRoute = keyRoute.slice(0, -1); |
|||
const properties = get(state, parentKeyRoute); |
|||
const newProperties: { [key: string]: SchemaItem } = {}; |
|||
|
|||
// keep the order of fields
|
|||
|
|||
// eslint-disable-next-line no-restricted-syntax
|
|||
for (const field in properties) { |
|||
if (field === oldField) { |
|||
newProperties[newField] = properties[field]; |
|||
} else { |
|||
newProperties[field] = properties[field]; |
|||
} |
|||
} |
|||
|
|||
set(state, parentKeyRoute, newProperties); |
|||
|
|||
// replace old field with new field in required
|
|||
changeRequiredField(state, keyRoute, newField); |
|||
}; |
|||
|
|||
const changeType = ( |
|||
state: SchemaState, |
|||
keyRoute: KeyRoute, |
|||
fieldType: SchemaType, |
|||
): void => { |
|||
// set field new value
|
|||
set(state, keyRoute, defaultSchema[fieldType]); |
|||
}; |
|||
|
|||
const changeTitle = ( |
|||
state: SchemaState, |
|||
keyRoute: KeyRoute, |
|||
title: string, |
|||
): void => { |
|||
// point to title and set new value
|
|||
const titleKeyRoute = keyRoute.concat('title'); |
|||
set(state, titleKeyRoute, title); |
|||
}; |
|||
|
|||
const changeDesc = ( |
|||
state: SchemaState, |
|||
keyRoute: KeyRoute, |
|||
desc: string, |
|||
): void => { |
|||
// point to desc and set new value
|
|||
const descKeyRoute = keyRoute.concat('description'); |
|||
set(state, descKeyRoute, desc); |
|||
}; |
|||
|
|||
const reducer = (state: SchemaState, action: SchemaAction): SchemaState => |
|||
produce(state, (draft: Draft<SchemaState>) => { |
|||
switch (action.type) { |
|||
case 'SET_SCHEMA': |
|||
setSchema(action.schema); |
|||
break; |
|||
|
|||
case 'ADD_CHILD_FIELD': |
|||
addChildField(draft, action.keyRoute); |
|||
break; |
|||
|
|||
case 'ADD_SIBLING_FIELD': |
|||
addSiblingField(draft, action.keyRoute); |
|||
break; |
|||
|
|||
case 'REMOVE_FIELD': |
|||
removeField(draft, action.keyRoute); |
|||
break; |
|||
|
|||
case 'TOGGLE_ALL_CHECKED': |
|||
toggleAllChecked(draft, action.checked); |
|||
break; |
|||
|
|||
case 'ADD_REQUIRED': |
|||
addRequired(draft, action.keyRoute); |
|||
break; |
|||
|
|||
case 'REMOVE_REQUIRED': |
|||
removeRequired(draft, action.keyRoute); |
|||
break; |
|||
|
|||
case 'CHANGE_FIELD': |
|||
changeField(draft, action.keyRoute, action.field); |
|||
break; |
|||
|
|||
case 'CHANGE_TYPE': |
|||
changeType(draft, action.keyRoute, action.fieldType); |
|||
break; |
|||
|
|||
case 'CHANGE_TITLE': |
|||
changeTitle(draft, action.keyRoute, action.title); |
|||
break; |
|||
|
|||
case 'CHANGE_DESC': |
|||
changeDesc(draft, action.keyRoute, action.desc); |
|||
break; |
|||
|
|||
default: |
|||
break; |
|||
} |
|||
}); |
|||
|
|||
export default reducer; |
@ -1,23 +0,0 @@ |
|||
import { Dispatch, useReducer } from 'react'; |
|||
import { createContainer } from 'react-tracked'; |
|||
import { SchemaState, SchemaAction } from '../model'; |
|||
import reducer from '../reducer/schema'; |
|||
|
|||
interface StoreProps { |
|||
initialData: SchemaState; |
|||
} |
|||
|
|||
const defaultData: SchemaState = { |
|||
type: 'object', |
|||
properties: {}, |
|||
required: [], |
|||
}; |
|||
|
|||
const useValue = ({ initialData }: StoreProps): [SchemaState, Dispatch<SchemaAction>] => |
|||
useReducer(reducer, initialData || defaultData); |
|||
|
|||
export const { |
|||
Provider: SchemaProvider, |
|||
useTrackedState: useSchemaState, |
|||
useUpdate: useSchemaDispatch, |
|||
} = createContainer(useValue); |
@ -1,13 +0,0 @@ |
|||
import { useState, Dispatch, SetStateAction } from 'react'; |
|||
import { createContainer } from 'react-tracked'; |
|||
import { UIState } from '../model'; |
|||
|
|||
const initialData = { properties: true }; |
|||
|
|||
const useValue = (): [UIState, Dispatch<SetStateAction<UIState>>] => useState<UIState>(initialData); |
|||
|
|||
export const { |
|||
Provider: UIProvider, |
|||
useTrackedState: useUIState, |
|||
useUpdate: useSetUIState, |
|||
} = createContainer(useValue); |
@ -1,60 +0,0 @@ |
|||
import { SchemaItem } from '../model'; |
|||
|
|||
export const defaultSchema = { |
|||
string: { |
|||
type: 'string', |
|||
}, |
|||
number: { |
|||
type: 'number', |
|||
}, |
|||
array: { |
|||
type: 'array', |
|||
items: { |
|||
type: 'string', |
|||
}, |
|||
}, |
|||
object: { |
|||
type: 'object', |
|||
properties: {}, |
|||
}, |
|||
boolean: { |
|||
type: 'boolean', |
|||
}, |
|||
integer: { |
|||
type: 'integer', |
|||
}, |
|||
}; |
|||
|
|||
export const schemaType = [ |
|||
'string', |
|||
'number', |
|||
'array', |
|||
'object', |
|||
'boolean', |
|||
'integer', |
|||
]; |
|||
|
|||
export const setAllFieldRequired = ( |
|||
schema: SchemaItem, |
|||
checked: boolean, |
|||
): void => { |
|||
if (schema.type === 'object') { |
|||
const { properties } = schema; |
|||
const fields = Object.keys(properties); |
|||
|
|||
if (checked) { |
|||
schema.required = fields; |
|||
} else { |
|||
delete schema.required; |
|||
} |
|||
|
|||
fields.forEach((field) => { |
|||
const childSchema = properties[field]; |
|||
if (['object', 'array'].includes(childSchema.type)) { |
|||
setAllFieldRequired(childSchema, checked); |
|||
} |
|||
}); |
|||
} else if (schema.type === 'array') { |
|||
setAllFieldRequired(schema.items, checked); |
|||
} |
|||
}; |
@ -1,9 +0,0 @@ |
|||
import 'antd/lib/row/style'; |
|||
import 'antd/lib/col/style'; |
|||
import 'antd/lib/menu/style'; |
|||
import 'antd/lib/input/style'; |
|||
import 'antd/lib/select/style'; |
|||
import 'antd/lib/button/style'; |
|||
import 'antd/lib/tooltip/style'; |
|||
import 'antd/lib/dropdown/style'; |
|||
import 'antd/lib/checkbox/style'; |
@ -1,52 +0,0 @@ |
|||
import { css } from '@emotion/core'; |
|||
import styled from '@emotion/styled'; |
|||
import { |
|||
CaretDownOutlined, |
|||
CaretRightOutlined, |
|||
EditOutlined, |
|||
SettingOutlined, |
|||
PlusOutlined, |
|||
CloseOutlined, |
|||
} from '@ant-design/icons'; |
|||
|
|||
const iconStyle = css` |
|||
vertical-align: middle; |
|||
cursor: pointer; |
|||
`;
|
|||
|
|||
const actionStyle = css` |
|||
margin: 0 5px; |
|||
svg { |
|||
font-size: 14px; |
|||
} |
|||
`;
|
|||
|
|||
export const CaretDownIcon = styled(CaretDownOutlined)` |
|||
${iconStyle} |
|||
`;
|
|||
|
|||
export const CaretRightIcon = styled(CaretRightOutlined)` |
|||
${iconStyle} |
|||
`;
|
|||
|
|||
export const EditIcon = styled(EditOutlined)` |
|||
${iconStyle} |
|||
`;
|
|||
|
|||
export const SettingIcon = styled(SettingOutlined)` |
|||
color: #00a854; |
|||
${iconStyle} |
|||
${actionStyle} |
|||
`;
|
|||
|
|||
export const PlusIcon = styled(PlusOutlined)` |
|||
color: #2395f1; |
|||
${iconStyle} |
|||
${actionStyle} |
|||
`;
|
|||
|
|||
export const CloseIcon = styled(CloseOutlined)` |
|||
color: #ff561b; |
|||
${iconStyle} |
|||
${actionStyle} |
|||
`;
|
@ -1,31 +0,0 @@ |
|||
import { css } from '@emotion/core'; |
|||
|
|||
const globalStyles = css` |
|||
.json-schema-editor { |
|||
* { |
|||
font-size: 12px; |
|||
} |
|||
|
|||
.ant-col { |
|||
:nth-of-type(2) { |
|||
text-align: center; |
|||
} |
|||
|
|||
:nth-of-type(3), |
|||
:nth-of-type(4) { |
|||
padding-right: 10px; |
|||
} |
|||
} |
|||
|
|||
.ant-input { |
|||
height: 30px; |
|||
} |
|||
|
|||
.ant-select { |
|||
width: 80%; |
|||
text-align: left; |
|||
} |
|||
} |
|||
`;
|
|||
|
|||
export default globalStyles; |
@ -1,7 +0,0 @@ |
|||
{ |
|||
"extends": "../../tsconfig", |
|||
"compilerOptions": { |
|||
"outDir": "dist/types" |
|||
}, |
|||
"include": ["./src", "./types"] |
|||
} |
Loading…
Reference in new issue