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