├── .editorconfig ├── .eslintrc.js ├── .fatherrc.ts ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .jest.config.ts ├── .prettierignore ├── .prettierrc.js ├── .umirc.ts ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── changelog.md ├── docs ├── guide │ └── index.md ├── index.md ├── index.zh-CN.md └── options.md ├── package-lock.json ├── package.json ├── public └── images │ ├── CPU-pixel.png │ └── favicon.ico ├── readme-dumi.md ├── src ├── JsonSchemaEditor │ ├── $meta.json │ ├── EditorDrawer.tsx │ ├── Field.tsx │ ├── __test__ │ │ ├── components │ │ │ └── core │ │ │ │ ├── EditorDrawer.test.tsx │ │ │ │ ├── customView.test.tsx │ │ │ │ └── hooks │ │ │ │ ├── useArrayCreator.test.tsx │ │ │ │ └── useObjectCreator.test.tsx │ │ ├── context │ │ │ ├── info.test.ts │ │ │ └── parse │ │ │ │ └── parse.test.tsx │ │ ├── definition │ │ │ ├── ajv.test.ts │ │ │ └── shallowValidate.test.ts │ │ ├── examples │ │ │ ├── basic.test.tsx │ │ │ ├── default.test.tsx │ │ │ ├── eslint.test.tsx │ │ │ ├── general.test.tsx │ │ │ ├── list.test.tsx │ │ │ └── simple.test.tsx │ │ ├── field │ │ │ └── rootMenuItems.test.tsx │ │ ├── helper │ │ │ └── processValidateErrors.test.ts │ │ ├── setupTest.tsx │ │ ├── test-utils │ │ │ ├── MockComponent.tsx │ │ │ ├── index.ts │ │ │ ├── interact-antd │ │ │ │ └── baseSelect.tsx │ │ │ └── sleep.ts │ │ └── utils │ │ │ ├── index.test.ts │ │ │ └── path │ │ │ └── path.test.ts │ ├── components │ │ ├── antd │ │ │ ├── SchemaErrorLogger.tsx │ │ │ ├── base │ │ │ │ ├── ListDisplayPanel.tsx │ │ │ │ ├── cacheInput.tsx │ │ │ │ └── creator │ │ │ │ │ ├── CreateName.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── config.tsx │ │ │ ├── container │ │ │ │ ├── FieldContainerNormal.tsx │ │ │ │ └── FieldContainerShort.tsx │ │ │ ├── css │ │ │ │ ├── data-item.less │ │ │ │ ├── index.less │ │ │ │ └── title.less │ │ │ ├── drawer │ │ │ │ └── FieldDrawer.tsx │ │ │ ├── edition │ │ │ │ ├── ArrayEdition.tsx │ │ │ │ ├── BooleanEdition.tsx │ │ │ │ ├── ConstEdition.tsx │ │ │ │ ├── EnumEdition.tsx │ │ │ │ ├── NullEdition.tsx │ │ │ │ ├── NumberEdition.tsx │ │ │ │ ├── ObjectEdition.tsx │ │ │ │ └── StringEdition.tsx │ │ │ ├── format │ │ │ │ ├── color-picker │ │ │ │ │ └── readme.md │ │ │ │ ├── date-time-picker │ │ │ │ │ └── readme.md │ │ │ │ ├── date-time.tsx │ │ │ │ ├── date.tsx │ │ │ │ ├── multiline.tsx │ │ │ │ ├── row.tsx │ │ │ │ └── time.tsx │ │ │ ├── index.tsx │ │ │ ├── operation │ │ │ │ ├── OneOf.tsx │ │ │ │ ├── OperationButton.tsx │ │ │ │ └── Type.tsx │ │ │ ├── title.tsx │ │ │ └── views │ │ │ │ └── list │ │ │ │ ├── ItemList.tsx │ │ │ │ └── index.tsx │ │ └── core │ │ │ ├── ComponentMap.ts │ │ │ ├── hooks │ │ │ ├── useArrayCreator.tsx │ │ │ ├── useArrayListContent.tsx │ │ │ ├── useFatherInfo.tsx │ │ │ ├── useFieldModel.tsx │ │ │ ├── useMenuActionComponents.tsx │ │ │ ├── useMenuActionHandlers.tsx │ │ │ ├── useObjectCreator.tsx │ │ │ ├── useObjectListContent.tsx │ │ │ └── useSubFieldQuery.tsx │ │ │ └── type │ │ │ ├── list.tsx │ │ │ └── props.ts │ ├── context │ │ ├── index.ts │ │ ├── interaction.ts │ │ ├── mergeSchema.ts │ │ ├── ofInfo.ts │ │ └── virtual.ts │ ├── definition │ │ ├── ajvInstance.ts │ │ ├── defaultValue.ts │ │ ├── formats.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── schema.ts │ │ └── shallowValidate.ts │ ├── demos │ │ ├── App.tsx │ │ ├── ModalSelect.tsx │ │ ├── basic-data │ │ │ ├── $schema.string-array.json │ │ │ └── string-array.json │ │ ├── examples.ts │ │ ├── feature │ │ │ └── of-first-choice.json │ │ ├── hooks.ts │ │ └── integrate │ │ │ ├── $meta.json │ │ │ ├── $schema.$meta.json │ │ │ ├── $schema.Classes.json │ │ │ ├── $schema.alternative.json │ │ │ ├── $schema.basic.json │ │ │ ├── $schema.dataFacility.json │ │ │ ├── $schema.dataSolar.json │ │ │ ├── $schema.dataTechTree.json │ │ │ ├── $schema.default.json │ │ │ ├── $schema.eslint.json │ │ │ ├── $schema.general.json │ │ │ ├── $schema.items.json │ │ │ ├── $schema.reducerTest.json │ │ │ ├── $schema.sceneConfig.json │ │ │ ├── $schema.simple.json │ │ │ ├── $schema.test-list.json │ │ │ ├── Classes.json │ │ │ ├── alternative.json │ │ │ ├── basic.json │ │ │ ├── dataFacility.json │ │ │ ├── dataSolar.json │ │ │ ├── dataTechTree.json │ │ │ ├── default.json │ │ │ ├── eslint.json │ │ │ ├── general.json │ │ │ ├── items.json │ │ │ ├── reducerTest.json │ │ │ ├── sceneConfig.json │ │ │ ├── simple.json │ │ │ └── test-list.json │ ├── helper │ │ └── validate-errors │ │ │ └── processValidateErrors.ts │ ├── index.md │ ├── index.tsx │ ├── menu │ │ └── MenuActions.ts │ ├── type │ │ ├── DirectActions.ts │ │ ├── Schema.ts │ │ └── ValueTypeMapper.ts │ └── utils │ │ ├── index.ts │ │ ├── path │ │ └── uri.ts │ │ └── schemaWithRef.ts ├── index.ts └── propsTest │ └── demos │ └── App.tsx ├── tsconfig.json └── typings.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | /** 5 | * recommended enabled/disabled rules for umi project 6 | * @note This is the ESLint configuration of UMi4 merged into the configuration of UMi3, based on recommended rule set from loaded eslint plugins 7 | */ 8 | 9 | const isTsProject = fs.existsSync(path.join(process.cwd() || '.', './tsconfig.json')) 10 | const isTypeAwareEnabled = process.env.DISABLE_TYPE_AWARE === undefined 11 | 12 | module.exports = { 13 | parser: '@babel/eslint-parser', 14 | plugins: ['@typescript-eslint', 'react', 'jest', 'react-hooks'], 15 | env: { 16 | browser: true, 17 | node: true, 18 | es6: true, 19 | mocha: true, 20 | jest: true, 21 | jasmine: true 22 | }, 23 | rules: { 24 | // eslint built-in rules 25 | // 不需要返回就用 forEach 26 | 'array-callback-return': 2, 27 | // eqeq 可能导致潜在的类型转换问题 28 | eqeqeq: 2, 29 | 'for-direction': 2, 30 | // 不加 hasOwnProperty 判断会多出原型链的内容 31 | 'guard-for-in': 2, 32 | 'no-async-promise-executor': 2, 33 | // case 的变量声明虽然确实有问题,但是不重名就不影响运行。不然在这个代码里面都得变成 if else 34 | // 'no-case-declarations': 2, 35 | 'no-debugger': 2, 36 | 'no-delete-var': 2, 37 | 'no-dupe-else-if': 2, 38 | 'no-duplicate-case': 2, 39 | // eval()可能导致潜在的安全问题 40 | 'no-eval': 2, 41 | 'no-ex-assign': 2, 42 | 'no-global-assign': 2, 43 | 'no-invalid-regexp': 2, 44 | // 没必要改 native 变量 45 | 'no-native-reassign': 2, 46 | // 修改对象时,会影响原对象;但是有些场景就是有目的 47 | // 'no-param-reassign': 2, 48 | // return 值无意义,可能会理解为 resolve 49 | 'no-promise-executor-return': 2, 50 | 'no-self-assign': 2, 51 | 'no-self-compare': 2, 52 | 'no-shadow-restricted-names': 2, 53 | 'no-sparse-arrays': 2, 54 | 'no-unsafe-finally': 2, 55 | 'no-unused-labels': 2, 56 | 'no-useless-catch': 2, 57 | 'no-useless-escape': 2, 58 | 'no-var': 2, 59 | 'no-with': 2, 60 | 'require-yield': 2, 61 | 'use-isnan': 2, 62 | 63 | // config-plugin-react rules 64 | // button 自带 submit 属性 65 | 'react/button-has-type': 2, 66 | 'react/jsx-key': 2, 67 | 'react/jsx-no-comment-textnodes': 2, 68 | 'react/jsx-no-duplicate-props': 2, 69 | 'react/jsx-no-target-blank': 2, 70 | 'react/jsx-no-undef': 2, 71 | 'react/jsx-uses-react': 2, 72 | 'react/jsx-uses-vars': 2, 73 | 'react/no-children-prop': 2, 74 | 'react/no-danger-with-children': 2, 75 | 'react/no-deprecated': 2, 76 | 'react/no-direct-mutation-state': 2, 77 | 'react/no-find-dom-node': 2, 78 | 'react/no-is-mounted': 2, 79 | 'react/no-string-refs': 2, 80 | 'react/no-render-return-value': 2, 81 | 'react/no-unescaped-entities': 2, 82 | 'react/no-unknown-property': 2, 83 | 'react/require-render-return': 2, 84 | 85 | // config-plugin-jest rules 86 | 'jest/no-conditional-expect': 2, 87 | 'jest/no-deprecated-functions': 2, 88 | 'jest/no-export': 2, 89 | 'jest/no-focused-tests': 2, 90 | 'jest/no-identical-title': 2, 91 | 'jest/no-interpolation-in-snapshots': 2, 92 | 'jest/no-jasmine-globals': 2, 93 | 'jest/no-jest-import': 2, 94 | 'jest/no-mocks-import': 2, 95 | 'jest/no-standalone-expect': 2, 96 | // 'jest/valid-describe-callback': 2, 97 | 'jest/valid-expect-in-promise': 2, 98 | 'jest/valid-expect': 2, 99 | 'jest/valid-title': 2, 100 | 101 | // config-plugin-react-hooks rules 102 | 'react-hooks/rules-of-hooks': 2 103 | // config-plugin-typescript rules 104 | }, 105 | overrides: isTsProject 106 | ? [ 107 | { 108 | files: ['**/*.{ts,tsx}'], 109 | parser: '@typescript-eslint/parser', 110 | rules: { 111 | '@typescript-eslint/ban-types': 2, 112 | '@typescript-eslint/no-confusing-non-null-assertion': 2, 113 | '@typescript-eslint/no-dupe-class-members': 2, 114 | // 对一个大的类型定义消参 115 | // '@typescript-eslint/no-empty-interface': 2, 116 | '@typescript-eslint/no-for-in-array': 2, 117 | '@typescript-eslint/no-invalid-this': 2, 118 | '@typescript-eslint/no-loop-func': 2, 119 | '@typescript-eslint/no-misused-new': 2, 120 | '@typescript-eslint/no-namespace': 2, 121 | '@typescript-eslint/no-non-null-asserted-optional-chain': 2, 122 | '@typescript-eslint/no-redeclare': 2, 123 | '@typescript-eslint/no-this-alias': 2, 124 | // '@typescript-eslint/no-unsafe-argument': 2, 125 | '@typescript-eslint/no-unused-expressions': 2, 126 | '@typescript-eslint/no-unused-vars': 2, 127 | '@typescript-eslint/no-use-before-define': 2, 128 | '@typescript-eslint/no-useless-constructor': 2, 129 | '@typescript-eslint/triple-slash-reference': 2 130 | } 131 | } 132 | ] 133 | : [], 134 | parserOptions: { 135 | ecmaFeatures: { 136 | jsx: true 137 | }, 138 | babelOptions: { 139 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], 140 | plugins: [ 141 | ['@babel/plugin-proposal-decorators', { legacy: true }], 142 | ['@babel/plugin-proposal-class-properties', { loose: true }] 143 | ] 144 | }, 145 | requireConfigFile: false, 146 | project: isTypeAwareEnabled ? './tsconfig.json' : undefined 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/umijs/father 2 | // 注意有些单词的大小写 3 | export default { 4 | esm: 'babel', 5 | umd: { 6 | file: 'index', 7 | sourcemap: true, 8 | }, 9 | extraBabelPlugins: [ 10 | // https://github.com/umijs/father#extrababelplugins 11 | [ 12 | 'babel-plugin-import', 13 | { 14 | libraryName: 'antd', 15 | libraryDirectory: 'es', 16 | style: true, 17 | }, 18 | ], 19 | ], 20 | lessInBabelMode: true, // https://github.com/umijs/father#lessinbabelmode 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /npm-debug.log* 6 | /yarn-error.log 7 | /yarn.lock 8 | /package-lock.json 9 | 10 | # production 11 | /es 12 | /docs-dist 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | /coverage 18 | 19 | # umi 20 | .umi 21 | .umi-production 22 | .umi-test 23 | .env.local 24 | 25 | # mfsu 26 | .mfsu-production 27 | 28 | # ide 29 | # /.vscode 30 | # /.idea 31 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.jest.config.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { isLernaPackage } from '@umijs/utils' 3 | import { existsSync } from 'fs' 4 | import { join } from 'path' 5 | // import type { Config } from '@jest/types' 6 | 7 | const testMatchTypes = ['spec', 'test', 'e2e'] 8 | 9 | const isLerna = isLernaPackage(process.cwd()) 10 | const hasPackage = false 11 | const testMatchPrefix = hasPackage ? `**/packages/1/` : '' 12 | const hasSrc = existsSync(join(process.cwd(), 'src')) 13 | 14 | const config = { 15 | collectCoverageFrom: [ 16 | 'index.{js,jsx,ts,tsx}', 17 | hasSrc && 'src/**/*.{js,jsx,ts,tsx}', 18 | isLerna && 'packages/*/src/**/*.{js,jsx,ts,tsx}', 19 | '!**/typings/**', 20 | '!**/types/**', 21 | '!**/fixtures/**', 22 | '!**/examples/**', 23 | '!**/*.d.ts' 24 | ].filter((dict) => typeof dict === 'string') as string[], 25 | coveragePathIgnorePatterns: ['/node_modules/', '.umi', '.umi-production', 'demos', '__test__'], 26 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'], 27 | moduleNameMapper: { 28 | '\\.(css|less|sass|scss|stylus)$': require.resolve('identity-obj-proxy') 29 | }, 30 | setupFiles: [ 31 | require.resolve('@umijs/test/helpers/setupFiles/shim'), 32 | require.resolve('./src/JsonSchemaEditor/__test__/setupTest') 33 | ], 34 | setupFilesAfterEnv: [require.resolve('@umijs/test/helpers/setupFiles/jasmine')], 35 | testEnvironment: require.resolve('jest-environment-jsdom-fourteen'), 36 | testMatch: [`${testMatchPrefix}**/?*.(${testMatchTypes.join('|')}).(j|t)s?(x)`], 37 | testPathIgnorePatterns: ['/node_modules/', '/fixtures/'], 38 | transform: { 39 | '^.+\\.(js|jsx|ts|tsx)$': require.resolve('@umijs/test/helpers/transformers/javascript'), 40 | '^.+\\.(css|less|sass|scss|stylus)$': require.resolve('@umijs/test/helpers/transformers/css'), 41 | '^(?!.*\\.(js|jsx|ts|tsx|css|less|sass|scss|stylus|json)$)': require.resolve( 42 | '@umijs/test/helpers/transformers/file' 43 | ) 44 | }, 45 | verbose: true, 46 | transformIgnorePatterns: [ 47 | // 加 [^/]*? 是为了兼容 tnpm 的目录结构 48 | // 比如:_umi-test@1.5.5@umi-test 49 | // `node_modules/(?!([^/]*?umi|[^/]*?umi-test)/)`, 50 | ], 51 | // 用于设置 jest worker 启动的个数 52 | ...(process.env.MAX_WORKERS ? { maxWorkers: Number(process.env.MAX_WORKERS) } : {}) 53 | } 54 | 55 | console.log('cpu-pro: 已使用 .jest.config.ts 作为 jest 配置文件。') 56 | 57 | export default config 58 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | **/*.ejs 3 | **/*.html 4 | package.json 5 | .umi 6 | .umi-production 7 | .umi-test 8 | 9 | .mfsu-production 10 | 11 | dist/* 12 | es/* 13 | docs-dist/* 14 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | singleQuote: true, 4 | trailingComma: 'none', 5 | semi: false, 6 | printWidth: 120, 7 | proseWrap: 'never', 8 | endOfLine: 'lf' 9 | } 10 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi' 2 | import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin' 3 | 4 | export default defineConfig({ 5 | title: 'json-schemaeditor-antd', 6 | favicon: '/images/favicon.ico', 7 | logo: '/images/CPU-pixel.png', 8 | outputPath: 'docs-dist', 9 | mode: 'site', 10 | // more config: https://d.umijs.org/config 11 | antd: { 12 | compact: true 13 | }, 14 | mfsu: { 15 | // production: { 16 | // output: '.mfsu-production' 17 | // } 18 | }, 19 | // https://umijs.org/zh-CN/guide/boost-compile-speed#monaco-editor-%E7%BC%96%E8%BE%91%E5%99%A8%E6%89%93%E5%8C%85 20 | chainWebpack: (config, { webpack }) => { 21 | config.plugin('monaco-editor-webpack-plugin').use(MonacoWebpackPlugin, [ 22 | { 23 | languages: ['json'] 24 | } 25 | ]) 26 | }, 27 | 28 | // dumi 文档发布需要做的事情 29 | history: { 30 | type: 'hash' 31 | }, 32 | // base: '/json-schemaeditor-antd/', 33 | publicPath: process.env.NODE_ENV === 'production' ? '/json-schemaeditor-antd/' : '/' 34 | }) 35 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-msedge", 9 | "request": "launch", 10 | "name": "Launch Edge against localhost", 11 | "url": "http://localhost:8000/#/~demos/jsonschemaeditor-app", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Jest All", 18 | "program": "${workspaceFolder}/node_modules/.bin/jest", 19 | "args": ["--runInBand", "--config", ".jest.config.ts"], 20 | "console": "integratedTerminal", 21 | "internalConsoleOptions": "neverOpen", 22 | "disableOptimisticBPs": true, 23 | "windows": { 24 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 25 | } 26 | }, 27 | { 28 | "type": "node", 29 | "request": "launch", 30 | "name": "Jest Current File", 31 | "program": "${workspaceFolder}/node_modules/.bin/jest", 32 | "args": ["${fileBasenameNoExtension}", "--config", ".jest.config.ts"], 33 | "console": "integratedTerminal", 34 | "internalConsoleOptions": "neverOpen", 35 | "disableOptimisticBPs": true, 36 | "windows": { 37 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 furtherBank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## [0.1.6](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.5...v0.1.6) (2023-04-02) 2 | 3 | ### Bug Fixes 4 | 5 | - 修复 schema 在局部变更时的组件展示问题 ([515a021](https://github.com/FurtherBank/json-schemaeditor-antd/commit/515a0215af43e02a0542123eae26586aeeba2c5e)) 6 | 7 | ### Features 8 | 9 | - date & time & date-time 组件 ([562139c](https://github.com/FurtherBank/json-schemaeditor-antd/commit/562139cde9f1713466c22c36164cb40f7ab2f669)) 10 | 11 | ## [0.1.5](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.4...v0.1.5) (2023-03-03) 12 | 13 | ### Bug Fixes 14 | 15 | - 一些代码问题 ([2f0884e](https://github.com/FurtherBank/json-schemaeditor-antd/commit/2f0884ea1ae380f748cfdd6fa8c4d064b4deb9b0)) 16 | - object 无法创建 additionalProperties 的 bug ([f595ffd](https://github.com/FurtherBank/json-schemaeditor-antd/commit/f595ffd8219aade3170afb423059616d9d8a1ec5)) 17 | 18 | ### Features 19 | 20 | - 按需验证 part 2 ([50a0a9d](https://github.com/FurtherBank/json-schemaeditor-antd/commit/50a0a9d6d6ec54964050ada4230770f4f060fbd9)) 21 | - ajv 按需验证 part1 ([f9299e2](https://github.com/FurtherBank/json-schemaeditor-antd/commit/f9299e249035dcc62e6ecea009aca7183a57586a)) 22 | 23 | ## [0.1.4](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.3...v0.1.4) (2023-01-01) 24 | 25 | ### Bug Fixes 26 | 27 | - 经过抽屉编辑后切换字段类型有可能会出错的问题 ([3d9e1db](https://github.com/FurtherBank/json-schemaeditor-antd/commit/3d9e1dbd9e8338d2754278b7c90c52d49cc7ed39)) 28 | - **parse:** 修复在 object 三大属性关键词都没有时,会出现数组访问报错的问题 ([1934a64](https://github.com/FurtherBank/json-schemaeditor-antd/commit/1934a64948c12a356e732b5f6d5076925643b02e)) 29 | - view: list 名称过长时样式问题 ([3711113](https://github.com/FurtherBank/json-schemaeditor-antd/commit/371111361e28fcd25285afe651914dab1602d954)) 30 | 31 | ### Features 32 | 33 | - 加入 reset 操作 ([cac0331](https://github.com/FurtherBank/json-schemaeditor-antd/commit/cac03310be93ca53a3bee7170b91b7d61028c797)) 34 | - 可使用自己的 componentMap ([0ac092c](https://github.com/FurtherBank/json-schemaeditor-antd/commit/0ac092c4025c6f5b98d17c3e4f9daedb7ecc743b)) 35 | - 可以给根节点设置更多菜单组件 rootMenuItems ([7d11e61](https://github.com/FurtherBank/json-schemaeditor-antd/commit/7d11e61c533cc839b25af6c90871ee75cb47f4b5)) 36 | - 组件层全剥离 ([cf2b32e](https://github.com/FurtherBank/json-schemaeditor-antd/commit/cf2b32ed98e8b5ba1c248ec844f2c7bd96d5a5a2)) 37 | - 组件架构分离 part1 ([13f4044](https://github.com/FurtherBank/json-schemaeditor-antd/commit/13f4044cd38beb7a6aa15635146d9e0302639a29)) 38 | - field 容器组件抽离 ([b380564](https://github.com/FurtherBank/json-schemaeditor-antd/commit/b380564e98ee3f1dd60d94db036dbbc484144ab8)) 39 | - view 特性实装 ([7dfbf99](https://github.com/FurtherBank/json-schemaeditor-antd/commit/7dfbf990cbe37b542586e4aa76568d78997917ce)) 40 | 41 | ## [0.1.3](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.2...v0.1.3) (2022-05-04) 42 | 43 | ### Bug Fixes 44 | 45 | - 修复若干 bug ([2618816](https://github.com/FurtherBank/json-schemaeditor-antd/commit/2618816e063afd0417628786f8aeb2bd3d06a933)) 46 | - 修复正则属性名修改验证无效的 bug ([3be7ed6](https://github.com/FurtherBank/json-schemaeditor-antd/commit/3be7ed6d5322564cbb11d176346f799ca2d896d2)) 47 | 48 | ### Features 49 | 50 | - 加入 id 功能 ([b29bc98](https://github.com/FurtherBank/json-schemaeditor-antd/commit/b29bc9856a38b838aa2b9adeef03124f9e97e09d)) 51 | 52 | ## [0.1.2](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.1...v0.1.2) (2022-05-04) 53 | 54 | ### Bug Fixes 55 | 56 | - 修复之前没改导致的 drawer 出不来的问题 ([a4ebcf3](https://github.com/FurtherBank/json-schemaeditor-antd/commit/a4ebcf3d7f5aed233ed1c05a0987854187c5f889)) 57 | 58 | ### Features 59 | 60 | - 动作空间按钮归类,加入 title 提示 ([b5ea106](https://github.com/FurtherBank/json-schemaeditor-antd/commit/b5ea106068091087726da582972124a4c6513e37)) 61 | - antd 不同主题兼容 ([0c33dc3](https://github.com/FurtherBank/json-schemaeditor-antd/commit/0c33dc3f6a08f77821cfdd8389b0cf0f1567ab8d)) 62 | 63 | ## [0.1.1](https://github.com/FurtherBank/json-schemaeditor-antd/compare/v0.1.0...v0.1.1) (2022-04-12) 64 | 65 | # 0.1.0 (2022-04-12) 66 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: 使用指导 4 | path: /guide 5 | --- 6 | 7 | # 使用指导 8 | 9 | ## schemaDefault 10 | 11 | > This field is used to specify the default value for the function field defined by the editor. 12 | 13 | displaydesc, notinautofill 14 | 15 | ### draggable 16 | 17 | > Whether an array item can be dragged. Even if set to true, drag-and-drop is limited by the items field in the schema. 18 | 19 | type: boolean 20 | 21 | default: true 22 | 23 | ## ui 24 | 25 | denseGrid, 26 | 27 | ## abc 28 | 29 | 这时意志 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: json-schemaeditor-antd 4 | desc: A JSON editor based on ant-design which can use JSON-Schema as constraint. 5 | actions: 6 | - text: Try now! 7 | link: /json-schema-editor 8 | features: 9 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png 10 | title: Good support 11 | desc: The editor has good support for JSON-schema features, and works well in variety cases of feature combinations. such as nested oneOf/anyOf with $ref and meta-schema editing. 12 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png 13 | title: Simple and efficient 14 | desc: You just need to set data, schema and onChange to use the editor. Compared with other json editing ways, such as editing with vscode, this editor is more efficient. 15 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d1ee0c6f-5aed-4a45-a507-339a4bfe076c/k7bjsocq_w144_h144.png 16 | title: With powerful 17 | desc: It works almost perfectly with any scenario of JSON editing. You can meet more personalized editing requirements by setting options. 18 | footer: Open-source MIT Licensed | Copyright © 2020Powered by [dumi](https://d.umijs.org) 19 | --- 20 | 21 | ## Then nothing here 22 | -------------------------------------------------------------------------------- /docs/index.zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: json-schemaeditor-antd 4 | desc: 基于 antd 搭建的可使用 JSON Schema 约束的 JSON 编辑器。\n支持良好,简单可靠,效率更高。 5 | actions: 6 | - text: 立刻体验 7 | link: /json-schema-editor 8 | features: 9 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/881dc458-f20b-407b-947a-95104b5ec82b/k79dm8ih_w144_h144.png 10 | title: 支持良好 11 | desc: 对 json-schema 特性支持良好,对各种特性组合的情况考虑深入,相对其它一些同类产品,支持 oneOf/anyOf 嵌套且组合 $ref、编辑元模式等特性功能。 12 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d60657df-0822-4631-9d7c-e7a869c2f21c/k79dmz3q_w126_h126.png 13 | title: 简单可靠 14 | desc: 无需配置,只需要传入 data 和 schema,以及 onChange 事件即可使用。相对于其它的 json 编辑方式(vscode编辑),具有更高的编辑效率。 15 | - icon: https://gw.alipayobjects.com/zos/bmw-prod/d1ee0c6f-5aed-4a45-a507-339a4bfe076c/k7bjsocq_w144_h144.png 16 | title: 功能强大 17 | desc: 几乎完全可以适用于任何使用 json 编辑的场景。可以对编辑器进行拓展,满足更多个性化编辑需求。 18 | footer: Open-source MIT Licensed | Copyright © 2020Powered by [dumi](https://d.umijs.org) 19 | --- 20 | 21 | ## Then nothing here 22 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://d.umijs.org/zh-CN/config/frontmatter 3 | nav: 4 | title: Options 5 | path: /options 6 | toc: menu 7 | --- 8 | 9 | # options 10 | 11 | > In most cases, you don't need to configure anything. 12 | 13 | idPerfix, defaultValueFunction, 14 | 15 | ## schemaDefault 16 | 17 | > This field is used to specify the default value for the function field defined by the editor. 18 | 19 | displaydesc, notinautofill 20 | 21 | ### draggable 22 | 23 | > Whether an array item can be dragged. Even if set to true, drag-and-drop is limited by the items field in the schema. 24 | 25 | type: boolean 26 | 27 | default: true 28 | 29 | ## ui 30 | 31 | denseGrid, 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": false, 3 | "name": "json-schemaeditor-antd", 4 | "version": "0.1.6", 5 | "description": "基于 antd 搭建的可使用 JSON Schema 约束的 JSON 编辑器。\n支持良好,简单可靠,效率更高。", 6 | "author": "furtherbank", 7 | "homepage": "https://github.com/furtherbank/json-schemaeditor-antd", 8 | "license": "MIT", 9 | "repository": "https://github.com/FurtherBank/json-schemaeditor-antd", 10 | "keywords": [ 11 | "antd", 12 | "ant-design", 13 | "react", 14 | "json-schema", 15 | "json", 16 | "json-editor" 17 | ], 18 | "scripts": { 19 | "start": "dumi dev", 20 | "docs:build": "dumi build", 21 | "docs:deploy": "gh-pages -d docs-dist", 22 | "build": "father-build", 23 | "deploy": "npm run docs:build && npm run docs:deploy", 24 | "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", 25 | "test": "jest --config .jest.config.ts", 26 | "cov": "jest --config .jest.config.ts --coverage", 27 | "prepublishOnly": "npm run build", 28 | "version": "conventional-changelog -p angular -i changelog.md -s && git add changelog.md", 29 | "prepare": "husky install" 30 | }, 31 | "files": [ 32 | "es", 33 | "dist" 34 | ], 35 | "main": "dist/index.min.js", 36 | "module": "es/index.js", 37 | "types": "es/index.d.ts", 38 | "commitlint": { 39 | "extends": [ 40 | "@commitlint/config-conventional" 41 | ] 42 | }, 43 | "lint-staged": { 44 | "*.{js,jsx,less,md,json}": [ 45 | "prettier --write" 46 | ], 47 | "*.ts?(x)": [ 48 | "prettier --parser=typescript --write" 49 | ] 50 | }, 51 | "peerDependencies": { 52 | "@ant-design/icons": "^4.0.0", 53 | "antd": "^4.0.0", 54 | "react": ">=16", 55 | "react-dom": ">=16" 56 | }, 57 | "dependencies": { 58 | "ajv": "^8.11.0", 59 | "ajv-formats": "^2.1.1", 60 | "ajv-i18n": "^4.2.0", 61 | "immer": "^9.0.12", 62 | "lodash": "^4.17.21", 63 | "react-redux": "^7.2.8", 64 | "react-selectable-fast": "^3.4.0", 65 | "redux": "^4.1.2", 66 | "redux-undo": "^1.0.1", 67 | "uuid": "^9.0.0" 68 | }, 69 | "devDependencies": { 70 | "@commitlint/cli": "^17.2.0", 71 | "@commitlint/config-conventional": "^17.3.0", 72 | "@testing-library/jest-dom": "^5.15.1", 73 | "@testing-library/react": "^12.1.2", 74 | "@testing-library/react-hooks": "^8.0.1", 75 | "@testing-library/user-event": "^14.4.3", 76 | "@types/jest": "^27.0.3", 77 | "@types/lodash": "^4.14.178", 78 | "@types/node": "^17.0.23", 79 | "@types/react": "^16.14.24", 80 | "@types/react-dom": "^16.9.14", 81 | "@types/uuid": "^9.0.0", 82 | "@umijs/fabric": "^2.8.1", 83 | "@umijs/preset-react": "^2.1.2", 84 | "@umijs/test": "^3.0.5", 85 | "babel-plugin-import": "^1.13.3", 86 | "conventional-changelog-cli": "^2.2.2", 87 | "dumi": "^1.1.0", 88 | "father-build": "^1.17.2", 89 | "gh-pages": "^3.0.0", 90 | "husky": "^8.0.0", 91 | "lesshat": "^4.1.0", 92 | "lint-staged": "^10.5.4", 93 | "monaco-editor": "^0.33.0", 94 | "monaco-editor-webpack-plugin": "^7.0.1", 95 | "prettier": "^2.2.1", 96 | "react-monaco-editor": "^0.47.0", 97 | "react-test-renderer": "^16.14.0", 98 | "ts-node": "^10.7.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /public/images/CPU-pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FurtherBank/json-schemaeditor-antd/5876a6fca38fb3a2c05be41bc89d9d9049847ef9/public/images/CPU-pixel.png -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FurtherBank/json-schemaeditor-antd/5876a6fca38fb3a2c05be41bc89d9d9049847ef9/public/images/favicon.ico -------------------------------------------------------------------------------- /readme-dumi.md: -------------------------------------------------------------------------------- 1 | # json-schemaeditor-antd 2 | 3 | ## Getting Started 4 | 5 | Install dependencies, 6 | 7 | ```bash 8 | $ npm i 9 | ``` 10 | 11 | Start the dev server, 12 | 13 | ```bash 14 | $ npm start 15 | ``` 16 | 17 | Build documentation, 18 | 19 | ```bash 20 | $ npm run docs:build 21 | ``` 22 | 23 | Run test, 24 | 25 | ```bash 26 | $ npm test 27 | ``` 28 | 29 | Build library via `father-build`, 30 | 31 | ```bash 32 | $ npm run build 33 | ``` 34 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/EditorDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useImperativeHandle, useState } from 'react' 2 | import Field from './Field' 3 | import { InfoContext } from '.' 4 | 5 | interface DrawerAccess { 6 | route: string[] | undefined 7 | field: string | undefined 8 | } 9 | 10 | const FieldDrawerBase = (props: any, ref: React.Ref | undefined) => { 11 | const ctx = useContext(InfoContext) 12 | const [access, setAccess] = useState({ 13 | route: undefined, 14 | field: undefined 15 | }) 16 | const [visible, setVisible] = useState(false) 17 | useImperativeHandle(ref, () => ({ 18 | setDrawer: (route: string[] | undefined, field: string | undefined, isVisible = true) => { 19 | setVisible(isVisible) 20 | setAccess({ 21 | route, 22 | field 23 | }) 24 | } 25 | })) 26 | 27 | const { route, field } = access 28 | 29 | const onClose = () => { 30 | setVisible(false) 31 | } 32 | 33 | const Drawer = ctx.getComponent(null, ['drawer']) 34 | 35 | return ( 36 | 37 | {route !== undefined && visible ? : null} 38 | 39 | ) 40 | } 41 | 42 | const FieldDrawer = React.forwardRef(FieldDrawerBase) 43 | 44 | export default FieldDrawer 45 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/components/core/EditorDrawer.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, act } from '@testing-library/react' 2 | import JsonSchemaEditor from '../../..' 3 | import CpuEditorContext from '../../../context' 4 | import { getExample } from '../../test-utils' 5 | import { MockRender } from '../../test-utils/MockComponent' 6 | 7 | it('not render field while not visible', async () => { 8 | const [data, schema] = getExample('一系列测试') 9 | const { current: ctx } = MockRender(JsonSchemaEditor, { data, schema }) 10 | 11 | // 点击 detail 12 | act(() => ctx.interaction.setDrawer(['mess'], '1')) 13 | 14 | // 关闭 drawer 15 | act(() => { 16 | const drawer = document.querySelector('.cpu-drawer')! 17 | 18 | const drawerClose = drawer.querySelector('.ant-drawer-close')! as HTMLElement 19 | 20 | fireEvent.click(drawerClose) 21 | }) 22 | 23 | ctx.executeAction('change', { route: [], field: 'mess', value: '' }) 24 | 25 | // 因为 immutable 性质,所以应当使用 ctx.getNowData 获取最新的 data 26 | expect(ctx.getNowData().mess).toBe('') 27 | }) 28 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/components/core/customView.test.tsx: -------------------------------------------------------------------------------- 1 | describe('view base feature ok', () => { 2 | it('view effects when dataSchema fulfilled', () => { 3 | expect(true).toBeTruthy() 4 | }) 5 | 6 | it('view not effects when dataSchema not fulfilled', () => { 7 | expect(true).toBeTruthy() 8 | }) 9 | }) 10 | 11 | describe('short rules with view', () => { 12 | it('recognize short with view.shortable', () => { 13 | expect(true).toBeTruthy() 14 | }) 15 | 16 | it('use short fallback when with view.shortable and dataSchema not fulfilled', () => { 17 | expect(true).toBeTruthy() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/components/core/hooks/useArrayCreator.test.tsx: -------------------------------------------------------------------------------- 1 | describe('get correct default value', () => { 2 | it('1', () => { 3 | expect(true).toBeTruthy() 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/components/core/hooks/useObjectCreator.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks' 2 | import { useObjectCreator } from '../../../../components/core/hooks/useObjectCreator' 3 | import { CpuEditorAction } from '../../../../definition/reducer' 4 | import { getExample, mockCtx } from '../../../test-utils' 5 | 6 | describe('useObjectCreator: get correct return value', () => { 7 | const [data, schema] = getExample('一系列测试') 8 | const ctx = mockCtx(data, schema) 9 | 10 | let createObjectPropOnNewNameTest: (name: string) => any 11 | renderHook(() => { 12 | createObjectPropOnNewNameTest = useObjectCreator( 13 | ctx, 14 | data['newNameTest'], 15 | ['newNameTest'], 16 | '#/properties/newNameTest', 17 | ctx.getMergedSchema('#/properties/newNameTest') 18 | ) 19 | }) 20 | 21 | let createObjectPropOnRoot: (name: string) => any 22 | renderHook(() => { 23 | createObjectPropOnRoot = useObjectCreator(ctx, data, [], '#', ctx.getMergedSchema('#')) 24 | }) 25 | 26 | it('return error message when field exists', () => { 27 | const result = createObjectPropOnNewNameTest('pro1') 28 | expect(typeof result).toBe('string') 29 | expect(result).toBe(`字段 pro1 已经存在!`) 30 | }) 31 | it('return error message when additionalProperties is not allowed and name is in additionalProperties', () => { 32 | const result = createObjectPropOnRoot('abcd') 33 | expect(typeof result).toBe('string') 34 | expect(result).toBe(`abcd 不匹配 properties 中的名称或 patternProperties 中的正则式`) 35 | }) 36 | it('create current value with properties', () => { 37 | const result = createObjectPropOnNewNameTest('pro4') 38 | expect(result).toEqual({ 39 | type: 'create', 40 | route: ['newNameTest'], 41 | field: 'pro4', 42 | schemaEntry: '#/properties/newNameTest', 43 | value: 4 44 | }) 45 | }) 46 | it('create current value with patternProperties', () => { 47 | const result = createObjectPropOnNewNameTest('pattern123') 48 | expect(result).toEqual({ 49 | type: 'create', 50 | route: ['newNameTest'], 51 | field: 'pattern123', 52 | schemaEntry: '#/properties/newNameTest', 53 | value: 19 54 | }) 55 | }) 56 | it('create current value with additionalProperties', () => { 57 | const result = createObjectPropOnNewNameTest('abcd') 58 | expect(result).toEqual({ 59 | type: 'create', 60 | route: ['newNameTest'], 61 | field: 'abcd', 62 | schemaEntry: '#/properties/newNameTest', 63 | value: 49 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/context/info.test.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { isShort } from '../../context/virtual' 3 | import { getExample, mockCtx } from '../test-utils' 4 | 5 | test('parse ok', () => { 6 | const [data, schema] = getExample('小型示例') 7 | const ctx = mockCtx(data, schema) 8 | const rootMerged = ctx.getMergedSchema('#/') 9 | const newProps = {} as any 10 | for (const key in schema.properties) { 11 | if (Object.prototype.hasOwnProperty.call(schema.properties, key)) { 12 | newProps[key] = '#/properties/' + key 13 | } 14 | } 15 | 16 | expect(rootMerged).toEqual( 17 | Object.assign(_.omit(schema, 'properties'), { 18 | type: [schema.type], 19 | properties: newProps, 20 | additionalProperties: false, 21 | [isShort]: false 22 | }) 23 | ) 24 | }) 25 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/context/parse/parse.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import JsonSchemaEditor from '../../..' 3 | import { MergedSchema } from '../../../context/mergeSchema' 4 | import { getExample, mockCtx } from '../../test-utils' 5 | import { MockRender } from '../../test-utils/MockComponent' 6 | import CpuEditorContext from '../../../context' 7 | 8 | it('use alternative rules correctly', () => { 9 | const [data, schema] = getExample('替代法则测试') 10 | const ctx = mockCtx(data, schema) 11 | const rootMerged = ctx.getMergedSchema('#/') as MergedSchema 12 | 13 | expect(rootMerged.format).toBe('multiline') 14 | expect(rootMerged.title).toBe('alternative') 15 | expect(rootMerged.description).toBe('#/definitions/a') 16 | }) 17 | 18 | it('parse in need', () => { 19 | const [data, schema] = getExample('view: list') 20 | // const { asFragment } = 21 | 22 | const { current: ctx } = MockRender(JsonSchemaEditor, { 23 | data, 24 | schema 25 | }) 26 | 27 | console.log(ctx.mergedSchemaMap.keys()) 28 | 29 | // only parse direct subField of array item 30 | expect(ctx.mergedSchemaMap.has('#/items/0')).toBe(true) 31 | expect(ctx.mergedSchemaMap.has('#/items/2')).toBe(true) 32 | expect(ctx.mergedSchemaMap.has('#/items/2/properties/name')).toBe(false) 33 | }) 34 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/definition/ajv.test.ts: -------------------------------------------------------------------------------- 1 | import ajvInstance from '../../definition/ajvInstance' 2 | import { getExample } from '../test-utils' 3 | 4 | test('draft4 support', () => { 5 | expect(true).toBeTruthy() 6 | }) 7 | 8 | test('errors of partial validation', () => { 9 | const [data, schema] = getExample('一系列测试') 10 | 11 | ajvInstance.addSchema(schema, 'id') 12 | 13 | const validate = ajvInstance.getSchema('id#/properties/mess')! 14 | validate(data.mess) 15 | 16 | console.log(validate.errors) 17 | 18 | expect(validate.errors?.length).toBe(1) 19 | expect(validate.errors![0]).toStrictEqual({ 20 | instancePath: '/1', 21 | schemaPath: '#/items/format', 22 | keyword: 'format', 23 | params: { format: 'color' }, 24 | message: 'must match format "color"' 25 | }) 26 | const validate2 = ajvInstance.getSchema('id')! 27 | expect(typeof validate2).toBe('function') 28 | }) 29 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/definition/shallowValidate.test.ts: -------------------------------------------------------------------------------- 1 | test('return true', () => { 2 | expect(true).toBeTruthy() 3 | }) 4 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/basic.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render, screen } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { getExample } from '../test-utils' 6 | 7 | test('basic', () => { 8 | const [data, schema] = getExample('基础') 9 | // const { asFragment } = 10 | render() 11 | // asserts 12 | const input = screen.getByDisplayValue(data) 13 | expect(input).toBeTruthy() 14 | }) 15 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/default.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { countNullId, getExample } from '../test-utils' 6 | 7 | test('default', () => { 8 | const [data, schema] = getExample('小型示例') 9 | // const { asFragment } = 10 | render() 11 | expect(countNullId(data)).toBe(0) 12 | }) 13 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/eslint.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { countNullId, getExample } from '../test-utils' 6 | 7 | test('eslint', () => { 8 | const [data, schema] = getExample('eslint(draft7)') 9 | // const { asFragment } = 10 | render() 11 | expect(countNullId(data)).toBeDefined() 12 | // allof 支持后做验证 13 | }) 14 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/general.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render, screen } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { countNullId, getExample } from '../test-utils' 6 | 7 | test('general', () => { 8 | const [data, schema] = getExample('一系列测试') 9 | // const { asFragment } = 10 | render() 11 | // asserts 12 | const textCanSeen = [ 13 | '一系列测试', 14 | 'enumValue', 15 | 'constValue', 16 | 'typeError', 17 | 'Array[5]', 18 | '又臭又长', 19 | '变量创建-命名测试', 20 | 'pro1', 21 | 'pro3', 22 | '混乱', 23 | '格式测试', 24 | // oneOf 套娃可看到的字符 25 | 'oneOf套娃 0', 26 | 'oneOf套娃 6', 27 | 'number[]', 28 | 'string[]', 29 | '类型为其它' 30 | ] 31 | // 这个地方必须加类型注解泛化这个对象的 keys,不然下面for in for of都会出错 32 | const multipleTextsCanSeen: { [text: string]: number } = { 33 | 类型为对象: 2, 34 | color: 2, 35 | date: 2 36 | } 37 | textCanSeen.forEach((text) => { 38 | expect(screen.getByText(text)).toBeTruthy() 39 | }) 40 | // 这个地方必须这样写。for in需要加hasOwnProperty判断然后导致test文件的avoid conditional expect规则违反 41 | for (const key of Object.keys(multipleTextsCanSeen)) { 42 | const length = multipleTextsCanSeen[key] 43 | expect(screen.getAllByText(key).length).toBe(length) 44 | } 45 | const displayValueCanSeen = ['如果你不喜欢现在的生活,那么你快去考研吧!', 'pattern567', 'balabala', '#ffffff'] 46 | displayValueCanSeen.forEach((text) => { 47 | expect(screen.getByDisplayValue(text)).toBeTruthy() 48 | }) 49 | expect(countNullId(data)).toBe(9) // enumValue(Array[5]) + constValue(Object[2]) + typeError(Object[2]) = 9 50 | }) 51 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/list.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { getExample } from '../test-utils' 6 | import { JSONSchema } from '../../type/Schema' 7 | 8 | test('view is list when schema has view.type === list', async () => { 9 | const [data, schema] = getExample('view: list') 10 | // const { asFragment } = 11 | render() 12 | // asserts 13 | const listItems = document.querySelectorAll('.list-item') 14 | expect(listItems).toHaveLength(data.length) 15 | }) 16 | 17 | test('view is not list when schema has view.type === list but data is not root', async () => { 18 | const schema = { 19 | $schema: 'http://json-schema.org/draft-06/schema#', 20 | type: 'object', 21 | properties: { 22 | a: { 23 | type: 'array', 24 | view: { 25 | type: 'list' 26 | }, 27 | items: [ 28 | { 29 | type: 'null' 30 | }, 31 | { 32 | type: 'string' 33 | } 34 | ], 35 | additionalItems: { 36 | type: 'integer' 37 | } 38 | } 39 | } 40 | } 41 | 42 | // const { asFragment } = 43 | render() 44 | // asserts 45 | const listItems = document.querySelectorAll('.list-item') 46 | expect(listItems).toHaveLength(0) 47 | }) 48 | 49 | test('view is not list when root schema is array but data not', async () => { 50 | const [, schema] = getExample('view: list') 51 | // const { asFragment } = 52 | render() 53 | // asserts 54 | const listItems = document.querySelectorAll('.list-item') 55 | expect(listItems).toHaveLength(0) 56 | }) 57 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/examples/simple.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import React from 'react' 3 | import { render } from '@testing-library/react' 4 | import JsonSchemaEditor from '../../..' 5 | import { countNullId, getExample } from '../test-utils' 6 | 7 | test('simple', () => { 8 | const [data, schema] = getExample('简单示例') 9 | // const { asFragment } = 10 | render() 11 | expect(countNullId(data)).toBe(0) 12 | }) 13 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/field/rootMenuItems.test.tsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, act, screen } from '@testing-library/react' 2 | import React, { useCallback, useImperativeHandle } from 'react' 3 | import { forwardRef, useRef } from 'react' 4 | import JsonSchemaEditor from '../../..' 5 | import CpuEditorContext from '../../context' 6 | import { getExample } from '../test-utils' 7 | import { MockRender } from '../test-utils/MockComponent' 8 | 9 | it('root menu items work', async () => { 10 | const [data, schema] = getExample('basic: string Array') 11 | 12 | const TestComponent = forwardRef((props, ref) => { 13 | const editorRef = useRef(null) 14 | 15 | useImperativeHandle(ref, () => editorRef.current!, [editorRef.current]) 16 | 17 | const pushItem = useCallback(() => { 18 | const ctx = editorRef.current 19 | if (ctx) { 20 | const data = ctx.getNowData() 21 | if (data instanceof Array) { 22 | const newItem = `new Item ${data.length}` 23 | ctx.executeAction('create', { route: [], field: data.length.toString(), value: newItem }) 24 | } 25 | } 26 | }, [editorRef]) 27 | const rootMenuItems = [ 28 | 29 | press to push item 30 | 31 | ] 32 | 33 | return 34 | }) as any 35 | 36 | const { current: ctx } = MockRender(TestComponent, {}) 37 | 38 | const rootMenuButton = screen.getByRole('button', { 39 | name: /press to push item/i 40 | }) 41 | 42 | expect(rootMenuButton).toBeTruthy() 43 | 44 | // 点击 rootMenuButton 45 | act(() => { 46 | fireEvent.click(rootMenuButton) 47 | }) 48 | 49 | // 因为 immutable 性质,所以应当使用 ctx.getNowData 获取最新的 data 50 | expect(ctx.getNowData().length).toBe(5) 51 | expect(ctx.getNowData()[4]).toBe('new Item 4') 52 | }) 53 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/helper/processValidateErrors.test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { getExample, mockCtx } from '../test-utils' 3 | 4 | it('partial validate errors ok', () => { 5 | const [data, schema] = getExample('一系列测试') 6 | const ctx = mockCtx(data, schema) 7 | 8 | const errors = ctx.store.getState().present.dataErrors 9 | const mess1Error = errors['/mess/1'] 10 | expect(mess1Error.length).toBe(1) 11 | expect(mess1Error[0].instancePath).toBe('/mess/1') 12 | expect(mess1Error[0].schemaPath).toBe('#/definitions/messDefForRefTest/items/format') 13 | 14 | // change mess/0 to error 15 | ctx.executeAction('change', { 16 | schemaEntry: '#/definitions/messDefForRefTest/items', 17 | route: ['mess'], 18 | field: '0', 19 | value: '12345644' 20 | }) 21 | 22 | const errors1 = ctx.store.getState().present.dataErrors 23 | const mess0Error = errors1['/mess/0'] 24 | expect(mess0Error.length).toBe(1) 25 | expect(mess0Error[0].instancePath).toBe('/mess/0') 26 | expect(mess0Error[0].schemaPath).toBe('#/definitions/messDefForRefTest/items/format') 27 | expect(errors1['/mess/1']).toBe(mess1Error) // 之前的错误引用未变,代表修改为局部不影响其它 28 | 29 | // fix mess/1 error 30 | ctx.executeAction('change', { 31 | schemaEntry: '#/definitions/messDefForRefTest/items', 32 | route: ['mess'], 33 | field: '1', 34 | value: '#123456' 35 | }) 36 | 37 | const errors2 = ctx.store.getState().present.dataErrors 38 | expect(errors2['/mess/1']).toBeUndefined() 39 | expect(errors2['/mess/0']).toBe(mess0Error) // 之前的错误引用未变,代表修改为局部不影响其它 40 | }) 41 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/setupTest.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // import { _rs as onLibResize } from 'rc-resize-observer/lib/utils/observerUtil'; 3 | // import { _rs as onEsResize } from 'rc-resize-observer/es/utils/observerUtil'; 4 | 5 | // eslint-disable-next-line no-console 6 | console.log('Current React Version:', React.version) 7 | 8 | // jest.mock('react', () => ({ 9 | // ...jest.requireActual('react'), 10 | // useLayoutEffect: jest.requireActual('react').useEffect, 11 | // })); 12 | 13 | /* eslint-disable global-require */ 14 | if (typeof window !== 'undefined') { 15 | global.window.resizeTo = (width, height) => { 16 | global.window.innerWidth = width || global.window.innerWidth 17 | global.window.innerHeight = height || global.window.innerHeight 18 | global.window.dispatchEvent(new Event('resize')) 19 | } 20 | global.window.scrollTo = () => {} 21 | // ref: https://github.com/ant-design/ant-design/issues/18774 22 | if (!window.matchMedia) { 23 | Object.defineProperty(global.window, 'matchMedia', { 24 | value: jest.fn((query) => ({ 25 | matches: query.includes('max-width'), 26 | addListener: jest.fn(), 27 | removeListener: jest.fn() 28 | })) 29 | }) 30 | } 31 | 32 | // Fix css-animation or rc-motion deps on these 33 | // https://github.com/react-component/motion/blob/9c04ef1a210a4f3246c9becba6e33ea945e00669/src/util/motion.ts#L27-L35 34 | // https://github.com/yiminghe/css-animation/blob/a5986d73fd7dfce75665337f39b91483d63a4c8c/src/Event.js#L44 35 | window.AnimationEvent = window.AnimationEvent || window.Event 36 | window.TransitionEvent = window.TransitionEvent || window.Event 37 | } 38 | 39 | // import Enzyme from 'enzyme' 40 | // import Adapter from 'enzyme-adapter-react-16' 41 | 42 | // Enzyme.configure({ adapter: new Adapter() }) 43 | 44 | // Object.assign(Enzyme.ReactWrapper.prototype, { 45 | // findObserver(index = 0) { 46 | // return this.find('ResizeObserver').at(index); 47 | // }, 48 | // triggerResize(index = 0) { 49 | // const target = this.findObserver(index).getDOMNode(); 50 | // const originGetBoundingClientRect = target.getBoundingClientRect; 51 | 52 | // target.getBoundingClientRect = () => ({ width: 510, height: 903 }); 53 | // onLibResize([{ target }]); 54 | // onEsResize([{ target }]); 55 | 56 | // target.getBoundingClientRect = originGetBoundingClientRect; 57 | // }, 58 | // }); 59 | 60 | // // React.StrictMode wrapper 61 | // jest.mock('enzyme', () => { 62 | // const enzyme = jest.requireActual('enzyme'); 63 | // const { StrictMode, cloneElement } = jest.requireActual('react'); 64 | // const { mount, render } = enzyme; 65 | 66 | // function EnzymeWrapper({ strictMode, children, ...props }) { 67 | // // Not wrap StrictMode for some test case need count render times 68 | // if (strictMode === false) { 69 | // return cloneElement(children, props); 70 | // } 71 | 72 | // return {cloneElement(children, props)}; 73 | // } 74 | 75 | // return { 76 | // ...enzyme, 77 | // mount: (ui, { strictMode, ...config } = {}, ...args) => 78 | // mount({ui}, config, ...args), 79 | // render: (ui, { strictMode, ...config } = {}, ...args) => 80 | // render({ui}, config, ...args), 81 | // originMount: mount, 82 | // }; 83 | // }); 84 | console.log('cpu-pro: 已使用 setupTest.tsx 作为 jest setup 文件。') 85 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/test-utils/MockComponent.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import userEvent from '@testing-library/user-event' 3 | import React, { ForwardRefExoticComponent, ForwardRefRenderFunction, useRef } from 'react' 4 | 5 | interface MockProps { 6 | refHook: any 7 | component: ForwardRefExoticComponent 8 | [k: string]: any 9 | } 10 | 11 | export class RefMockerHook { 12 | constructor(public current: any = {}) {} 13 | } 14 | 15 | /** 16 | * mock 包装组件,可以提取出 ref 17 | * @param props 18 | * @returns 19 | */ 20 | export const RefMocker = (props: MockProps) => { 21 | const { refHook, component: RenderComponent, ...restProps } = props 22 | const ref = useRef(null) 23 | 24 | refHook.current = ref 25 | 26 | return 27 | } 28 | 29 | export const MockRender = (component: ForwardRefRenderFunction, props: any = {}) => { 30 | const user = userEvent.setup() 31 | const refMockerHook = new RefMockerHook() 32 | const renderResult = render() 33 | 34 | const current = refMockerHook.current.current as T 35 | 36 | return { current, refMockerHook, renderResult, user } 37 | } 38 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | import { metaSchema } from '../../..' 2 | import CpuEditorContext from '../../context' 3 | import examples from '../../demos/examples' 4 | import { CpuInteraction } from '../../context/interaction' 5 | import { IComponentMap, IViewsMap } from '../../components/core/ComponentMap' 6 | import Ajv from 'ajv' 7 | import defaultAjvInstance from '../../definition/ajvInstance' 8 | import { antdComponentMap, antdViewsMap } from '../../components/antd' 9 | 10 | export const getAllObjectRefs = (data: any, ref = ''): string[] => { 11 | const result = [] 12 | if (data && typeof data === 'object') { 13 | for (const key in data) { 14 | if (Object.prototype.hasOwnProperty.call(data, key)) { 15 | const value = data[key] 16 | const currentRef = ref ? ref + '.' + key.toString() : key.toString() 17 | result.push(currentRef) 18 | result.push(...getAllObjectRefs(value, currentRef)) 19 | } 20 | } 21 | } 22 | return result 23 | } 24 | 25 | /** 26 | * 得到 data 所有的 keyref,并根据 keyref 获得所有同名 id 的元素 27 | * @param data 28 | * @returns 29 | */ 30 | export const getKeysAndIds = (data: any) => { 31 | const allRefs = getAllObjectRefs(data) 32 | const allElements: (Element | null)[] = [] 33 | allRefs.forEach((ref) => { 34 | allElements.push(document.getElementById(ref)) 35 | }) 36 | return [allRefs, allElements] as [string[], (Element | null)[]] 37 | } 38 | 39 | /** 40 | * 通过 data 所有的 keyref 数量和 id 数量比较,得出几个 key 被隐藏了。 41 | * @param data 42 | * @returns 43 | */ 44 | export const countNullId = (data: any) => { 45 | const [, allElements] = getKeysAndIds(data) 46 | return allElements.filter((element) => !element).length 47 | } 48 | 49 | export const getExample = (name: string) => { 50 | const exampleJson = examples(metaSchema) 51 | return exampleJson[name] || [0, {}] 52 | } 53 | 54 | /** 55 | * 不需要组件的情况下构造 ctx,便于测试 56 | * 57 | * 该函数会随着 ctx 的构造函数参数改动而改动 58 | * @param data 59 | * @param schema 60 | * @param ajvInstance 61 | * @param id 62 | * @param componentMap 63 | * @param viewsMap 64 | * @returns 65 | */ 66 | export const mockCtx = ( 67 | data: any, 68 | schema: any, 69 | ajvInstance?: Ajv, 70 | id?: string, 71 | componentMap?: IComponentMap, 72 | viewsMap?: Record 73 | ) => { 74 | const interaction = new CpuInteraction(() => {}) 75 | return new CpuEditorContext( 76 | data, 77 | schema, 78 | ajvInstance ?? defaultAjvInstance, 79 | id, 80 | interaction, 81 | componentMap ?? antdComponentMap, 82 | viewsMap ?? antdViewsMap 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/test-utils/interact-antd/baseSelect.tsx: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react' 2 | import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup' 3 | import { sleep } from '../sleep' 4 | 5 | /** 6 | * 未编写完成的操作 baseSelect 组件的方法 7 | * @param user 8 | * @param dom 9 | */ 10 | export const changeBaseSelect = async (user: UserEvent, dom: Element, targetValue: string) => { 11 | await act(async () => { 12 | // 切换为 string 13 | const messTypeSelector = dom.querySelector('.ant-select-selector')! as HTMLElement 14 | 15 | await user.click(messTypeSelector) 16 | }) 17 | 18 | // 由于点击呼出 list 时,会短暂的不可点击,所以等待一段时间 19 | await sleep(2000) 20 | 21 | await act(async () => { 22 | const userEventNoneNode = document.querySelector('.ant-select-dropdown')! as HTMLElement 23 | userEventNoneNode.style.pointerEvents = 'auto' 24 | const stringItemContainer = document.querySelector( 25 | `.ant-select-item-option[title="${targetValue}"]` 26 | )! as HTMLElement 27 | 28 | await user.click(stringItemContainer) 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/test-utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (time = 1000) => { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve() 5 | }, time) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/utils/index.test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { 3 | addRef, 4 | concatAccess, 5 | deepCollect, 6 | deepGet, 7 | deepReplace, 8 | deepSet, 9 | getValueByPattern, 10 | jsonDataType 11 | } from '../../utils' 12 | import _ from 'lodash' 13 | import { uri2strArray } from '../../utils/path/uri' 14 | 15 | describe('utils', () => { 16 | it('concatAccess: ok', () => { 17 | const route = ['abc', 'def', 'ghi'] 18 | expect(concatAccess(route, null)).toEqual(['abc', 'def', 'ghi']) 19 | expect(concatAccess(route, 'a')).toEqual(['abc', 'def', 'ghi', 'a']) 20 | expect(concatAccess(route, '')).toEqual(['abc', 'def', 'ghi']) 21 | 22 | // expect(screen.queryByText(basic)).toBeInTheDocument(); 23 | }) 24 | 25 | it('addRef', () => { 26 | expect(addRef('#/title', 'foo', 'bar')).toBe('#/title/foo/bar') 27 | expect(addRef('#/title/', 'foo', 'bar')).toBe('#/title/foo/bar') 28 | expect(addRef(undefined, '#/title/aabc')).toBe(undefined) 29 | }) 30 | 31 | it('getRefSchemaMap', () => { 32 | expect(addRef('#/title', 'foo', 'bar')).toBe('#/title/foo/bar') 33 | expect(addRef('#/title/', 'foo', 'bar')).toBe('#/title/foo/bar') 34 | expect(addRef(undefined, '#/title/aabc')).toBe(undefined) 35 | }) 36 | 37 | it('deepGet', () => { 38 | const json = { 39 | title: 'Default Schema', 40 | description: 'a simple object schema by default', 41 | type: 'object', 42 | properties: { 43 | key: { 44 | type: 'string', 45 | format: 'row' 46 | } 47 | }, 48 | additionalProperties: false 49 | } 50 | expect(deepGet(json, uri2strArray('#/title'))).toBe(json.title) 51 | expect(deepGet(json, uri2strArray('#/title/aabc'))).toBe(undefined) 52 | expect(deepGet(json, uri2strArray('#/properties'))).toBe(json.properties) 53 | expect(deepGet(json, uri2strArray('#/properties/key/'))).toBe(json.properties.key) 54 | expect(deepGet(json, uri2strArray('#/'))).toBe(json) 55 | }) 56 | 57 | it('deepCollect', () => { 58 | const obj = { 59 | a: { 60 | a: 5, 61 | b: 3, 62 | c: [] 63 | }, 64 | b: { 65 | a: [ 66 | 1, 67 | 2, 68 | { 69 | a: 5, 70 | b: 3 71 | } 72 | ], 73 | b: 4 74 | } 75 | } 76 | expect(deepCollect(obj, 'a')).toEqual([ 77 | { 78 | a: 5, 79 | b: 3, 80 | c: [] 81 | }, 82 | [ 83 | 1, 84 | 2, 85 | { 86 | a: 5, 87 | b: 3 88 | } 89 | ] 90 | ]) 91 | expect(deepCollect(obj, 'b')).toEqual([ 92 | 3, 93 | { 94 | a: [ 95 | 1, 96 | 2, 97 | { 98 | a: 5, 99 | b: 3 100 | } 101 | ], 102 | b: 4 103 | } 104 | ]) 105 | expect(deepCollect(obj, 'c')).toEqual([[]]) 106 | expect(deepCollect(obj, 'd')).toEqual([]) 107 | }) 108 | 109 | it('deepReplace', () => { 110 | const obj = { 111 | a: { 112 | a: 5, 113 | b: 3, 114 | c: [] 115 | }, 116 | b: { 117 | a: [ 118 | 1, 119 | 2, 120 | { 121 | a: 5, 122 | b: 3 123 | } 124 | ], 125 | b: 4 126 | } 127 | } 128 | const obj2 = _.cloneDeep(obj) 129 | const replace = (value: any) => { 130 | switch (jsonDataType(value)) { 131 | case 'object': 132 | return null 133 | case 'array': 134 | return value.concat(66) 135 | case 'number': 136 | return value + 1 137 | case 'string': 138 | return value + 'balabala' 139 | default: 140 | return value 141 | } 142 | } 143 | expect(deepReplace(_.cloneDeep(obj), 'a', replace)).toEqual({ 144 | a: null, 145 | b: { 146 | a: [ 147 | 1, 148 | 2, 149 | { 150 | a: 5, 151 | b: 3 152 | }, 153 | 66 154 | ], 155 | b: 4 156 | } 157 | }) 158 | expect(deepReplace(_.cloneDeep(obj), 'b', replace)).toEqual({ 159 | a: { 160 | a: 5, 161 | b: 4, 162 | c: [] 163 | }, 164 | b: null 165 | }) 166 | expect(deepReplace(_.cloneDeep(obj), 'c', replace)).toEqual({ 167 | a: { 168 | a: 5, 169 | b: 3, 170 | c: [66] 171 | }, 172 | b: { 173 | a: [ 174 | 1, 175 | 2, 176 | { 177 | a: 5, 178 | b: 3 179 | } 180 | ], 181 | b: 4 182 | } 183 | }) 184 | expect(deepReplace(_.cloneDeep(obj), 'd', replace)).toEqual(obj2) 185 | }) 186 | 187 | it('deepSet', () => { 188 | const json = { 189 | title: 'Default Schema', 190 | description: 'a simple object schema by default', 191 | type: 'object', 192 | properties: { 193 | key: { 194 | type: 'string', 195 | format: 'row' 196 | } 197 | }, 198 | additionalProperties: false 199 | } as any 200 | const json0 = _.cloneDeep(json) 201 | const json1 = _.cloneDeep(json) 202 | json1.title = 'Schema' 203 | const json2 = _.cloneDeep(json) 204 | json2.properties.key.maxLength = 20 205 | const json3 = _.cloneDeep(json) 206 | json3.properties.key = null 207 | const json4 = _.cloneDeep(json) 208 | json4.properties.a = { 209 | b: { 210 | c: { 211 | d: null 212 | } 213 | } 214 | } 215 | expect(deepSet(_.cloneDeep(json), '#/title', 'Schema')).toEqual(json1) 216 | expect(deepSet(_.cloneDeep(json), '#/title/aabc', 5)).toEqual(json0) 217 | expect(deepSet(_.cloneDeep(json), '#/properties/key/maxLength', 20)).toEqual(json2) 218 | expect(deepSet(_.cloneDeep(json), '#/properties/key/', null)).toEqual(json3) 219 | expect(deepSet(_.cloneDeep(json), '#/properties/a/b/c/d', null)).toEqual(json4) 220 | }) 221 | 222 | it('getValueByPattern', () => { 223 | const obj = { 224 | 'pattern[0-9]+': 1 225 | } 226 | expect(getValueByPattern(obj, 'pattern[0-9]+')).toBeUndefined() 227 | expect(getValueByPattern(obj, 'pattern1233')).toBe(1) 228 | expect(getValueByPattern(obj, 'patter1233')).toBeUndefined() 229 | }) 230 | }) 231 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/__test__/utils/path/path.test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { uri2strArray } from '../../../utils/path/uri' 3 | 4 | describe('path', () => { 5 | it('uri2strArray', () => { 6 | expect(uri2strArray('#/abc/def')).toEqual(['abc', 'def']) 7 | expect(uri2strArray('#/abc/def/')).toEqual(['abc', 'def']) 8 | expect(uri2strArray('#/')).toEqual([]) 9 | // expect(screen.queryByText(basic)).toBeInTheDocument(); 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/SchemaErrorLogger.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'antd' 2 | import React from 'react' 3 | 4 | export const SchemaErrorLogger = (props: { error: string }) => { 5 | const { error } = props 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/base/ListDisplayPanel.tsx: -------------------------------------------------------------------------------- 1 | import { List } from 'antd' 2 | import React from 'react' 3 | import { CreateName } from '../base/creator' 4 | import { ShortLevel } from '../../../definition' 5 | import { ChildData, EmptyChildData, ListDisplayPanelProps } from '../../core/type/list' 6 | import { useSubFieldQuery } from '../../core/hooks/useSubFieldQuery' 7 | import { gridOption } from '../config' 8 | 9 | export const ListDisplayPanel = (props: ListDisplayPanelProps) => { 10 | const { viewport, data, access, fieldInfo, fatherInfo, lists } = props 11 | const { schemaEntry } = fatherInfo 12 | const { ctx, mergedValueSchema } = fieldInfo 13 | 14 | const getSubField = useSubFieldQuery(data, access, fieldInfo, fatherInfo, viewport) 15 | 16 | const renderItem = (shortLv: ShortLevel) => { 17 | return (item: ChildData | EmptyChildData) => { 18 | if (item.key !== '') { 19 | const { key } = item 20 | return {getSubField(key, shortLv)} 21 | } else { 22 | return ( 23 | 24 | 31 | 32 | ) 33 | } 34 | } 35 | } 36 | 37 | // const keys = Object.keys(data) 38 | // // todo: 排查属性的 order 关键字并写入 cache,然后在这里排个序再 map 39 | // const renderItems = keys.map((key: number | string) => { 40 | // return 48 | // }) 49 | // // 创建新属性组件 50 | // if (canCreate) renderItems.push( 51 | // 56 | // ) 57 | 58 | // return ( 59 | // 64 | // {renderItems} 65 | // 66 | // ) 67 | return ( 68 | 69 | {lists.map((list, i) => { 70 | const { items, short } = list 71 | return ( 72 | 80 | ) 81 | })} 82 | 83 | ) 84 | // switch (1) { 85 | // case 'list': 86 | // return ( 87 | // 88 | // 123 | // 124 | // {data.length > 0 ? getSubField(currentItem.toString(), short) : null} 125 | // 126 | // 127 | // ) 128 | // default: 129 | // } 130 | } 131 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/base/cacheInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input, InputNumber, AutoComplete, AutoCompleteProps } from 'antd' 2 | import React, { useState } from 'react' 3 | const { TextArea } = Input 4 | 5 | interface InputComProps { 6 | value?: any 7 | onChange?: (e: React.SyntheticEvent | any) => void 8 | onBlur?: (e: any) => void 9 | [k: string]: any 10 | } 11 | 12 | interface CachedComProps 13 | extends Pick { 14 | onValueChange?: (e: any) => void 15 | validate: boolean | ((v: any) => boolean) 16 | [k: string]: any 17 | } 18 | 19 | /** 20 | * 构建缓存式 input 组件的 HOC。 21 | * 传入 input 组件,输出后,得到缓存式的 input 组件。 22 | * 缓存式组件在失去焦点(onBlur)后才会发出状态更新请求。 23 | * 这时可以对输入进行验证,不通过可以阻止其更新,回到之前的输入。 24 | * @param InputComponent Input 组件。可以是普通的 input,也可以是封装的 input React.ComponentType 25 | * @returns 26 | */ 27 | const cacheInput = (InputComponent: React.ComponentType): React.FC => { 28 | return ({ 29 | value, 30 | onValueChange, 31 | validate, 32 | onBlur, 33 | // autoComplete props 34 | backfill, 35 | defaultActiveFirstOption, 36 | options, 37 | filterOption, 38 | open, 39 | ...props 40 | }) => { 41 | const [cache, setCache] = useState(value) // cache总是input的属性 42 | const [prev, setPrev] = useState(value) 43 | 44 | // onChange 更新 cache。支持 onChange 事件拿到的是 value 或 DOM事件两种情况 45 | const onChange = (e: any) => { 46 | if (e !== null) { 47 | const value = typeof e === 'object' && e.hasOwnProperty('currentTarget') ? e.currentTarget.value : e 48 | setCache(value) 49 | } 50 | } 51 | 52 | const newOnBlur = (e: { currentTarget: { value: any } }) => { 53 | const valid = typeof validate === 'boolean' ? validate : validate(cache) 54 | if (valid) { 55 | setPrev(cache) 56 | // 调用 onValueChange,告诉父组件可以改 value 属性了 57 | if (onValueChange && typeof onValueChange === 'function') onValueChange(cache) 58 | } else { 59 | setCache(value) 60 | } 61 | // 如果有 onBlur 一并执行 62 | if (onBlur && typeof onBlur === 'function') onBlur(e) 63 | } 64 | 65 | // 如果之前的value不同于现在的value,就是外部属性引起的value更新,此时同步cache 66 | if (prev !== value) { 67 | setPrev(value) 68 | setCache(value) 69 | } 70 | 71 | const autoCompleteFields = { 72 | backfill, 73 | defaultActiveFirstOption, 74 | options, 75 | open, 76 | filterOption 77 | } 78 | return options ? ( 79 | 80 | 81 | 82 | ) : ( 83 | 84 | ) 85 | } 86 | } 87 | 88 | export const CInput = cacheInput(Input), 89 | CInputNumber = cacheInput(InputNumber), 90 | CTextArea = cacheInput(TextArea) 91 | 92 | export default cacheInput 93 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/base/creator/CreateName.tsx: -------------------------------------------------------------------------------- 1 | import { CheckOutlined, CloseOutlined, PlusOutlined } from '@ant-design/icons' 2 | import { AutoComplete, Button, Input, message } from 'antd' 3 | import React, { useCallback, useState } from 'react' 4 | import CpuEditorContext from '../../../../context' 5 | import { MergedSchema } from '../../../../context/mergeSchema' 6 | import { useArrayCreator } from '../../../core/hooks/useArrayCreator' 7 | import { useObjectCreator } from '../../../core/hooks/useObjectCreator' 8 | 9 | export interface CreateNameProps { 10 | ctx: CpuEditorContext 11 | access: string[] 12 | data: any 13 | schemaEntry: string | undefined 14 | mergedValueSchema: MergedSchema | false | undefined 15 | style?: React.CSSProperties 16 | } 17 | 18 | export const ObjectPropCreator = (props: CreateNameProps) => { 19 | const { data, access, style, schemaEntry, mergedValueSchema, ctx } = props 20 | 21 | const [editing, setEditing] = useState(false) 22 | const [name, setName] = useState('') 23 | 24 | const { properties } = mergedValueSchema || {} 25 | 26 | // todo: 考察 notInAutoFill 以及 create 是否允许 27 | const optionStrings = properties 28 | ? Object.keys(properties).filter((key) => { 29 | return data[key] === undefined 30 | }) 31 | : [] 32 | const autoCompleteOptions = optionStrings.map((key) => { 33 | return { value: key } 34 | }) 35 | 36 | // name 编辑交互 37 | const handleClick = useCallback(() => { 38 | setEditing(!editing) 39 | }, [editing, setEditing]) 40 | 41 | const handleNameChange = (value: string) => { 42 | setName(value) 43 | } 44 | 45 | // 从 object 创建 prop 事件 46 | const createObjectProp = useObjectCreator(ctx, data, access, schemaEntry, mergedValueSchema) 47 | 48 | const handleObjectCreate = useCallback(() => { 49 | const actionOrError = createObjectProp(name) 50 | if (typeof actionOrError === 'string') { 51 | message.error(actionOrError) 52 | } else { 53 | setEditing(false) 54 | } 55 | }, [name, createObjectProp, setEditing]) 56 | 57 | return editing ? ( 58 | 59 | option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1} 62 | value={name} 63 | onChange={handleNameChange} 64 | style={{ flex: '1' }} 65 | > 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ) : ( 77 | } size="small" block onClick={handleClick} style={style}> 78 | Property 79 | 80 | ) 81 | } 82 | 83 | export const ArrayItemCreator = (props: CreateNameProps) => { 84 | const { data, access, style, mergedValueSchema, ctx, schemaEntry } = props 85 | 86 | const createArrayItem = useArrayCreator(ctx, data, access, mergedValueSchema, schemaEntry) 87 | 88 | return ( 89 | } size="small" block onClick={createArrayItem} style={style}> 90 | Item 91 | 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/base/creator/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { jsonDataType } from '../../../../utils' 3 | import { ArrayItemCreator, CreateNameProps, ObjectPropCreator } from './CreateName' 4 | 5 | export const CreateName = (props: CreateNameProps) => { 6 | const { data } = props 7 | const type = jsonDataType(data) 8 | return type === 'object' ? : 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/config.tsx: -------------------------------------------------------------------------------- 1 | export const maxCollapseLayer = 3 2 | // export const maxItemsPerPageByShortLevel = [16, 32, 48] 3 | 4 | export const gridOption = [ 5 | { gutter: 2, column: 1 }, 6 | { gutter: 2, column: 2, lg: 2, xl: 2, xxl: 2 }, 7 | { gutter: 2, column: 4, lg: 4, xl: 4, xxl: 4 } 8 | ] 9 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/container/FieldContainerNormal.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Collapse, Space } from 'antd' 2 | import React from 'react' 3 | import { getFormatType } from '../../../definition/formats' 4 | import { useMenuActionComponents } from '../../core/hooks/useMenuActionComponents' 5 | import { jsonDataType, concatAccess, getAccessRef } from '../../../utils' 6 | import { ContainerProps } from '../../core/type/props' 7 | import { maxCollapseLayer } from '../config' 8 | 9 | const { Panel } = Collapse 10 | 11 | const stopBubble = (e: React.SyntheticEvent) => { 12 | e.stopPropagation() 13 | } 14 | 15 | export const FieldContainerNormal = (props: ContainerProps) => { 16 | const { data, route, field, fieldDomId, titleComponent, valueComponent, rootMenuItems = [], fieldInfo } = props 17 | const { mergedValueSchema } = fieldInfo 18 | 19 | const dataType = jsonDataType(data) 20 | const access = concatAccess(route, field) 21 | 22 | const [operationComponents, menuActionComponents] = useMenuActionComponents(props) 23 | 24 | const { format } = mergedValueSchema || {} 25 | const formatType = getFormatType(format) 26 | 27 | const dataIsObject = dataType === 'object' || dataType === 'array' 28 | const canCollapse = dataIsObject && access.length > 0 29 | const editionIsMultiline = dataIsObject || formatType === 2 30 | 31 | const extraComponents = operationComponents.concat(rootMenuItems).concat(menuActionComponents) 32 | 33 | return canCollapse ? ( 34 | 38 | {extraComponents}} 42 | id={getAccessRef(access) || fieldDomId} 43 | > 44 | {valueComponent} 45 | 46 | 47 | ) : ( 48 | 53 | {!editionIsMultiline ? valueComponent : null} 54 | {extraComponents} 55 | 56 | } 57 | bodyStyle={!editionIsMultiline ? { display: 'none' } : {}} 58 | id={getAccessRef(access) || fieldDomId} 59 | className="cpu-field" 60 | > 61 | {editionIsMultiline ? valueComponent : null} 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/container/FieldContainerShort.tsx: -------------------------------------------------------------------------------- 1 | import { EllipsisOutlined } from '@ant-design/icons' 2 | import { Button, Dropdown, Input, Menu } from 'antd' 3 | import React from 'react' 4 | import { jsonDataType } from '../../../utils' 5 | import { ContainerProps } from '../../core/type/props' 6 | 7 | export const FieldContainerShort = (props: ContainerProps) => { 8 | const { data, fieldDomId, availableMenuActions, menuActionHandlers, titleComponent, valueComponent, fieldInfo } = 9 | props 10 | const { mergedValueSchema } = fieldInfo 11 | 12 | // 这里单独拿出来是为防止 ts 认为是 undefined 13 | 14 | const dataType = jsonDataType(data) 15 | const { const: constValue, enum: enumValue } = mergedValueSchema || {} 16 | 17 | const valueType = constValue !== undefined ? 'const' : enumValue !== undefined ? 'enum' : dataType 18 | 19 | const menuAction = (e: { key: string }) => { 20 | const key = e.key as keyof typeof menuActionHandlers 21 | if (menuActionHandlers[key]) menuActionHandlers[key]() 22 | } 23 | 24 | const items = availableMenuActions.map((a: string) => { 25 | return {a} 26 | }) 27 | const menu = {items} 28 | 29 | const compact = valueType !== 'boolean' 30 | return ( 31 | 32 | {titleComponent} 33 | 43 | {valueComponent} 44 | {items.length !== 0 ? ( 45 | 46 | } size="small" shape="circle" /> 47 | 48 | ) : null} 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/css/data-item.less: -------------------------------------------------------------------------------- 1 | .list-item { 2 | box-sizing: border-box; 3 | height: 1em; 4 | padding: 0.25em 0.5em; 5 | 6 | &:not(:last-child) { 7 | border-bottom: solid 1px #80808040; 8 | } 9 | } 10 | 11 | .list-item > p { 12 | padding-right: 0.5em; 13 | color: #808080; 14 | font-size: smaller; 15 | } 16 | 17 | .list-item > span { 18 | overflow: hidden; 19 | white-space: nowrap; 20 | text-overflow: ellipsis; 21 | } 22 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/css/index.less: -------------------------------------------------------------------------------- 1 | .selectable-selectbox { 2 | position: absolute; 3 | z-index: 9000; 4 | background: none; 5 | border: 1px dashed grey; 6 | cursor: default; 7 | } 8 | 9 | // 用于解决 flex 布局下,selector 项内容过长导致的布局问题 10 | .resolve-flex .ant-select-selection-item { 11 | width: 0; 12 | } 13 | 14 | // utils css style 15 | 16 | .flex-center { 17 | display: flex; 18 | align-items: center; 19 | } 20 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/css/title.less: -------------------------------------------------------------------------------- 1 | .inline-text-block { 2 | display: inline-block; 3 | overflow: hidden; 4 | white-space: nowrap; 5 | } 6 | 7 | .prop-name { 8 | padding: 0; 9 | text-overflow: ellipsis; 10 | } 11 | 12 | .prop-name-editable { 13 | text-decoration: underline; 14 | .prop-name(); 15 | } 16 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/drawer/FieldDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Drawer } from 'antd' 3 | import { EditorDrawerProps } from '../../core/type/props' 4 | 5 | export const FieldDrawer = (props: EditorDrawerProps) => { 6 | const { onClose, visible, children } = props 7 | 8 | return ( 9 | 17 | {children} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/ArrayEdition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { concatAccess } from '../../../utils' 3 | import { EditionProps } from '../../core/type/props' 4 | import { ConstEdition } from './ConstEdition' 5 | import { useArrayListContent } from '../../core/hooks/useArrayListContent' 6 | import { useFatherInfo } from '../../core/hooks/useFatherInfo' 7 | import { ListDisplayPanel } from '../base/ListDisplayPanel' 8 | 9 | const ArrayEditionPanel = (props: EditionProps) => { 10 | const { viewport, data, route, field, schemaEntry, fieldInfo } = props 11 | const { valueEntry, mergedValueSchema } = fieldInfo 12 | 13 | console.assert(data instanceof Array) 14 | 15 | const access = useMemo(() => { 16 | return concatAccess(route, field) 17 | }, [route, field]) 18 | 19 | const fatherInfo = useFatherInfo(data, schemaEntry, valueEntry, mergedValueSchema) 20 | 21 | const lists = useArrayListContent(data, schemaEntry, fieldInfo) 22 | 23 | return ( 24 | 32 | ) 33 | } 34 | 35 | export const ArrayEdition = (props: EditionProps) => { 36 | const { short } = props 37 | return short ? : 38 | } 39 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/BooleanEdition.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd' 2 | import React, { useCallback } from 'react' 3 | import { EditionProps } from '../../core/type/props' 4 | 5 | export const BooleanEdition = (props: EditionProps) => { 6 | const { 7 | route, 8 | field, 9 | data, 10 | schemaEntry, 11 | fieldInfo: { ctx } 12 | } = props 13 | 14 | const handleValueChange = useCallback( 15 | (value: any) => { 16 | if (value !== undefined) ctx.executeAction('change', { schemaEntry, route, field, value }) 17 | }, 18 | [ctx] 19 | ) 20 | 21 | return ( 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/ConstEdition.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Space } from 'antd' 2 | import React from 'react' 3 | import { toConstName } from '../../../definition' 4 | import { EditionProps } from '../../core/type/props' 5 | 6 | export const ConstEdition = (props: EditionProps) => { 7 | const { data } = props 8 | 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/EnumEdition.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Select } from 'antd' 2 | import isEqual from 'lodash/isEqual' 3 | import React, { useCallback } from 'react' 4 | import { toConstName } from '../../../definition' 5 | import { EditionProps } from '../../core/type/props' 6 | 7 | export const EnumEdition = (props: EditionProps) => { 8 | const { 9 | data, 10 | route, 11 | field, 12 | fieldInfo: { ctx, mergedValueSchema }, 13 | schemaEntry 14 | } = props 15 | const { enum: enumValue = [] } = mergedValueSchema || {} 16 | 17 | const handleValueChange = useCallback( 18 | (key: string) => { 19 | const i = parseInt(key) 20 | const value = enumValue[i] 21 | 22 | if (value !== undefined) ctx.executeAction('change', { schemaEntry, route, field, value }) 23 | }, 24 | [ctx] 25 | ) 26 | 27 | const enumIndex = enumValue.findIndex((v) => isEqual(v, data)) 28 | 29 | return ( 30 | 31 | { 35 | return { 36 | value: i, 37 | label: toConstName(value) 38 | } 39 | })} 40 | className="resolve-flex" 41 | style={{ flex: 1 }} 42 | onChange={handleValueChange} 43 | value={enumIndex === -1 ? '' : toConstName(enumValue[enumIndex])} 44 | allowClear={false} 45 | /> 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/NullEdition.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const NullEdition = () => { 4 | return null 5 | } 6 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/NumberEdition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { EditionProps } from '../../core/type/props' 3 | import { CInputNumber } from '../base/cacheInput' 4 | 5 | export const NumberEdition = (props: EditionProps) => { 6 | const { 7 | route, 8 | field, 9 | schemaEntry, 10 | fieldInfo: { ctx } 11 | } = props 12 | const data = props.data as number 13 | 14 | const handleValueChange = useCallback( 15 | (value: number) => { 16 | ctx.executeAction('change', { schemaEntry, route, field, value }) 17 | }, 18 | [ctx] 19 | ) 20 | 21 | return ( 22 | { 29 | e.target.blur() 30 | }} 31 | style={{ flex: 1 }} 32 | /> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/ObjectEdition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import { concatAccess } from '../../../utils' 3 | import { EditionProps } from '../../core/type/props' 4 | import { ConstEdition } from './ConstEdition' 5 | import { useObjectListContent } from '../../core/hooks/useObjectListContent' 6 | import { useFatherInfo } from '../../core/hooks/useFatherInfo' 7 | import { ListDisplayPanel } from '../base/ListDisplayPanel' 8 | 9 | const ObjectEditionPanel = (props: EditionProps) => { 10 | const { viewport, data, route, field, schemaEntry, fieldInfo } = props 11 | const { valueEntry, mergedValueSchema } = fieldInfo 12 | 13 | console.assert(typeof data === 'object' && !(data instanceof Array)) 14 | 15 | const access = useMemo(() => { 16 | return concatAccess(route, field) 17 | }, [route, field]) 18 | 19 | const fatherInfo = useFatherInfo(data, schemaEntry, valueEntry, mergedValueSchema) 20 | 21 | const lists = useObjectListContent(data, schemaEntry, fieldInfo) 22 | 23 | return ( 24 | 32 | ) 33 | } 34 | 35 | export const ObjectEdition = (props: EditionProps) => { 36 | const { short } = props 37 | return short ? : 38 | } 39 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/edition/StringEdition.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { CInput } from '../base/cacheInput' 3 | import { EditionProps } from '../../core/type/props' 4 | 5 | const allUsedProps = { 6 | size: 'small', 7 | key: 'value', 8 | validate: true 9 | } 10 | 11 | export const StringEdition = (props: EditionProps) => { 12 | const { 13 | route, 14 | field, 15 | data, 16 | schemaEntry, 17 | fieldInfo: { ctx } 18 | } = props 19 | 20 | const handleValueChange = useCallback( 21 | (value: string) => { 22 | if (value !== undefined) ctx.executeAction('change', { schemaEntry, route, field, value }) 23 | }, 24 | [ctx] 25 | ) 26 | 27 | return ( 28 | { 34 | e.currentTarget.blur() 35 | }} 36 | /> 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/color-picker/readme.md: -------------------------------------------------------------------------------- 1 | # color-picker 2 | 3 | 该组件是对`react-color`的重构 4 | 5 | ## supported formats 6 | 7 | `color` 8 | `color: rgba` 9 | `color: hsv` 10 | 11 | ## supported views 12 | 13 | `color` 14 | `color: hsv` 15 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/date-time-picker/readme.md: -------------------------------------------------------------------------------- 1 | # date-time-picker 2 | 3 | 该组件对`antd`的`date-picker`进行了一层封装,由于`antd`需要使用`moment.js`所以这个封装是必然的 4 | 5 | ## supported formats 6 | 7 | `color` 8 | `color: rgba` 9 | `color: hsv` 10 | 11 | ## supported views 12 | 13 | `color` 14 | `color: hsv` 15 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/date-time.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react' 2 | import { FormatEditionProps } from '../../core/type/props' 3 | import { DatePicker } from 'antd' 4 | import moment, { Moment } from 'moment' 5 | 6 | export const DateTimeEdition = (props: FormatEditionProps) => { 7 | const { 8 | route, 9 | field, 10 | data, 11 | schemaEntry, 12 | fieldInfo: { ctx } 13 | } = props 14 | 15 | const dateValue = useMemo(() => { 16 | return moment(data) 17 | }, [data]) 18 | 19 | const handleValueChange = useCallback( 20 | (value: Moment | null, dateString: string) => { 21 | if (value !== undefined) ctx.executeAction('change', { route, field, value: dateString, schemaEntry }) 22 | }, 23 | [ctx] 24 | ) 25 | 26 | return ( 27 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/date.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react' 2 | import { FormatEditionProps } from '../../core/type/props' 3 | import { DatePicker } from 'antd' 4 | import moment, { Moment } from 'moment' 5 | 6 | export const DateEdition = (props: FormatEditionProps) => { 7 | const { 8 | route, 9 | field, 10 | data, 11 | schemaEntry, 12 | fieldInfo: { ctx } 13 | } = props 14 | 15 | const dateValue = useMemo(() => { 16 | return moment(data) 17 | }, [data]) 18 | 19 | const handleValueChange = useCallback( 20 | (value: Moment | null, dateString: string) => { 21 | if (value !== undefined) ctx.executeAction('change', { route, field, value: dateString, schemaEntry }) 22 | }, 23 | [ctx] 24 | ) 25 | 26 | return ( 27 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/multiline.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { CTextArea } from '../base/cacheInput' 3 | import { FormatEditionProps } from '../../core/type/props' 4 | 5 | export const MultilineEdition = (props: FormatEditionProps) => { 6 | const { 7 | route, 8 | field, 9 | data, 10 | schemaEntry, 11 | fieldInfo: { ctx } 12 | } = props 13 | 14 | const handleValueChange = useCallback( 15 | (value: any) => { 16 | if (value !== undefined) ctx.executeAction('change', { schemaEntry, route, field, value }) 17 | }, 18 | [ctx] 19 | ) 20 | 21 | return ( 22 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/row.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { CInput } from '../base/cacheInput' 3 | import { FormatEditionProps } from '../../core/type/props' 4 | 5 | export const RowEdition = (props: FormatEditionProps) => { 6 | const { 7 | route, 8 | field, 9 | data, 10 | schemaEntry, 11 | fieldInfo: { ctx } 12 | } = props 13 | 14 | const handleValueChange = useCallback( 15 | (value: any) => { 16 | if (value !== undefined) ctx.executeAction('change', { route, field, value, schemaEntry }) 17 | }, 18 | [ctx] 19 | ) 20 | 21 | return ( 22 | { 29 | e.currentTarget.blur() 30 | }} 31 | style={{ flex: 1, minWidth: '400px' }} 32 | /> 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/format/time.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react' 2 | import { FormatEditionProps } from '../../core/type/props' 3 | import { TimePicker } from 'antd' 4 | import moment, { Moment } from 'moment' 5 | 6 | export const TimeEdition = (props: FormatEditionProps) => { 7 | const { 8 | route, 9 | field, 10 | data, 11 | schemaEntry, 12 | fieldInfo: { ctx } 13 | } = props 14 | 15 | const dateValue = useMemo(() => { 16 | return moment(data, 'HH:mm:ss') 17 | }, [data]) 18 | 19 | const handleValueChange = useCallback( 20 | (value: Moment | null, dateString: string) => { 21 | if (value !== undefined) ctx.executeAction('change', { route, field, value: dateString, schemaEntry }) 22 | }, 23 | [ctx] 24 | ) 25 | 26 | return ( 27 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/index.tsx: -------------------------------------------------------------------------------- 1 | import { FieldDrawer } from './drawer/FieldDrawer' 2 | import { IComponentMap } from '../core/ComponentMap' 3 | import { BooleanEdition } from './edition/BooleanEdition' 4 | import { ConstEdition } from './edition/ConstEdition' 5 | import { EnumEdition } from './edition/EnumEdition' 6 | import { NullEdition } from './edition/NullEdition' 7 | import { NumberEdition } from './edition/NumberEdition' 8 | import { StringEdition } from './edition/StringEdition' 9 | import { MultilineEdition } from './format/multiline' 10 | import { RowEdition } from './format/row' 11 | import { FieldTitle } from './title' 12 | import { OperationButton } from './operation/OperationButton' 13 | import { OneOfOperation } from './operation/OneOf' 14 | import { TypeOperation } from './operation/Type' 15 | import { FieldContainerNormal } from './container/FieldContainerNormal' 16 | import { FieldContainerShort } from './container/FieldContainerShort' 17 | import { SchemaErrorLogger } from './SchemaErrorLogger' 18 | import { ArrayListViewEdition } from './views/list' 19 | 20 | import './css/index.less' 21 | import { ObjectEdition } from './edition/ObjectEdition' 22 | import { ArrayEdition } from './edition/ArrayEdition' 23 | import { DateEdition } from './format/date' 24 | import { DateTimeEdition } from './format/date-time' 25 | import { TimeEdition } from './format/time' 26 | 27 | export const antdComponentMap: IComponentMap = { 28 | containerNormal: FieldContainerNormal, 29 | containerShort: FieldContainerShort, 30 | title: FieldTitle, 31 | menuAction: OperationButton, 32 | operation: { 33 | oneOf: OneOfOperation, 34 | type: TypeOperation 35 | }, 36 | format: { 37 | multiline: MultilineEdition, 38 | date: DateEdition, 39 | time: TimeEdition, 40 | 'date-time': DateTimeEdition, 41 | row: RowEdition, 42 | uri: RowEdition, 43 | 'uri-reference': RowEdition 44 | }, 45 | edition: { 46 | object: ObjectEdition, 47 | array: ArrayEdition, 48 | string: StringEdition, 49 | number: NumberEdition, 50 | boolean: BooleanEdition, 51 | null: NullEdition, 52 | enum: EnumEdition, 53 | const: ConstEdition 54 | }, 55 | drawer: FieldDrawer, 56 | schemaErrorLogger: SchemaErrorLogger 57 | } 58 | 59 | export const antdViewsMap = { 60 | list: { 61 | edition: { 62 | array: ArrayListViewEdition 63 | }, 64 | shortable: false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/operation/OneOf.tsx: -------------------------------------------------------------------------------- 1 | import { TreeSelect } from 'antd' 2 | import React from 'react' 3 | import { ofSchemaCache } from '../../../context/ofInfo' 4 | 5 | interface OneOfOperationProps { 6 | opHandler: (value: string) => void 7 | opValue: string 8 | opParam: ofSchemaCache 9 | } 10 | 11 | export const OneOfOperation = (props: OneOfOperationProps) => { 12 | const { opHandler, opValue, opParam } = props 13 | return ( 14 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/operation/OperationButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ArrowUpOutlined, 3 | ArrowDownOutlined, 4 | DeleteOutlined, 5 | UndoOutlined, 6 | RedoOutlined, 7 | RetweetOutlined 8 | } from '@ant-design/icons' 9 | import { Button } from 'antd' 10 | import React from 'react' 11 | import { MenuActionProps } from '../../core/type/props' 12 | 13 | const actionIcon = { 14 | reset: , 15 | moveup: , 16 | movedown: , 17 | delete: , 18 | undo: , 19 | redo: , 20 | detail: null 21 | } 22 | 23 | export const OperationButton = (props: MenuActionProps) => { 24 | const { opType, opHandler } = props 25 | return ( 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/operation/Type.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'antd' 2 | import React from 'react' 3 | 4 | interface TypeOperationProps { 5 | opHandler: (value: string) => void 6 | opValue: string 7 | opParam: string[] 8 | } 9 | 10 | export const TypeOperation = (props: TypeOperationProps) => { 11 | const { opHandler, opValue, opParam } = props 12 | return ( 13 | { 17 | return { value: value, label: value } 18 | })} 19 | onChange={opHandler} 20 | value={opValue} 21 | allowClear={false} 22 | style={{ width: '80px' }} 23 | /> 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/title.tsx: -------------------------------------------------------------------------------- 1 | import { CloseCircleOutlined } from '@ant-design/icons' 2 | import { Tooltip } from 'antd' 3 | import React from 'react' 4 | import { ShortLevel } from '../../definition' 5 | import { canFieldRename, isFieldRequired } from '../../definition/schema' 6 | 7 | import { CInput } from '../antd/base/cacheInput' 8 | import { EditionProps } from '../core/type/props' 9 | 10 | import './css/title.less' 11 | 12 | const stopBubble = (e: React.SyntheticEvent) => { 13 | e.stopPropagation() 14 | } 15 | 16 | export const FieldTitle = (props: EditionProps) => { 17 | const { route, field, short, canNotRename, fatherInfo, fieldInfo } = props 18 | const { errors, mergedEntrySchema, ctx } = fieldInfo 19 | const { schemaEntry: parentSchemaEntry } = fatherInfo ?? {} 20 | const { description } = mergedEntrySchema || {} 21 | 22 | const fieldNameRange = canFieldRename(props, fieldInfo) 23 | const titleName = fieldNameRange === '' || fieldNameRange instanceof RegExp ? field : fieldNameRange 24 | 25 | const isRequired = isFieldRequired(field, fatherInfo) 26 | 27 | const spaceStyle = 28 | short === ShortLevel.short 29 | ? { 30 | width: '9.5em' 31 | } 32 | : {} 33 | return ( 34 | 35 | {errors.length > 0 ? ( 36 | error.message).join('\n')} 38 | placement="topLeft" 39 | key="valid" 40 | > 41 | 42 | 43 | ) : null} 44 | 45 | {short !== ShortLevel.extra ? ( 46 | 47 | {!canNotRename && (fieldNameRange === '' || fieldNameRange instanceof RegExp) ? ( 48 | { 55 | return fieldNameRange instanceof RegExp ? fieldNameRange.test(v) : true 56 | }} 57 | onPressEnter={(e: any) => { 58 | e.currentTarget.blur() 59 | }} 60 | onValueChange={(value) => { 61 | ctx.executeAction('rename', { route, field, value, schemaEntry: parentSchemaEntry }) 62 | }} 63 | /> 64 | ) : ( 65 | 66 | {isRequired ? * : null} 67 | {titleName} 68 | 69 | )} 70 | 71 | ) : null} 72 | 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/views/list/ItemList.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, PropsWithChildren } from 'react' 2 | import { toConstName } from '../../../../definition' 3 | import { createSelectable, TSelectableItemProps } from 'react-selectable-fast' 4 | 5 | import '../../css/data-item.less' 6 | import { ChildData } from '../../../core/type/list' 7 | 8 | type Props = { 9 | items: ChildData[] 10 | } 11 | 12 | export const DataItem = createSelectable( 13 | (props: TSelectableItemProps & PropsWithChildren) => { 14 | const { selectableRef, isSelected, isSelecting, children, id } = props 15 | 16 | const classNames = [ 17 | 'ant-select-item ant-select-item-option list-item', 18 | false, 19 | isSelecting && 'ant-select-item-option-active', 20 | isSelected && 'ant-select-item-option-selected' 21 | ] 22 | .filter(Boolean) 23 | .join(' ') 24 | 25 | return ( 26 | 27 | {id} 28 | {children} 29 | 30 | ) 31 | } 32 | ) 33 | 34 | export type DataItemProps = { 35 | id: number 36 | } 37 | 38 | export const ItemList = memo((props: Props) => { 39 | const { items } = props 40 | 41 | return ( 42 | 43 | {items.map((item, i) => { 44 | const { value } = item 45 | return {`${toConstName(value)}`} 46 | })} 47 | 48 | ) 49 | }) 50 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/antd/views/list/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react' 2 | import { CreateName } from '../../base/creator' 3 | import { concatAccess, jsonDataType } from '../../../../utils' 4 | import { EditionProps } from '../../../core/type/props' 5 | import { SelectableGroup } from 'react-selectable-fast' 6 | import { DataItemProps, ItemList } from './ItemList' 7 | import { ChildData } from '../../../core/type/list' 8 | import { useFatherInfo } from '../../../core/hooks/useFatherInfo' 9 | import { useArrayListContent } from '../../../core/hooks/useArrayListContent' 10 | import { useSubFieldQuery } from '../../../core/hooks/useSubFieldQuery' 11 | 12 | const ArrayListView = (props: EditionProps) => { 13 | const { viewport, data, route, field, schemaEntry, fieldInfo } = props 14 | const { valueEntry, ctx, mergedValueSchema } = fieldInfo 15 | 16 | const dataType = jsonDataType(data) as 'object' | 'array' 17 | console.assert(dataType === 'object' || dataType === 'array') 18 | 19 | const access = useMemo(() => { 20 | return concatAccess(route, field) 21 | }, [route, field]) 22 | 23 | const fatherInfo = useFatherInfo(data, schemaEntry, valueEntry, mergedValueSchema) 24 | 25 | const lists = useArrayListContent(data, schemaEntry, fieldInfo) 26 | 27 | const lastList = lists[lists.length - 1].items 28 | const canCreate = lastList[lastList.length - 1] === null 29 | 30 | const content = useMemo(() => { 31 | // 展平 list,且将最后的 { key: '' } 去除掉 32 | const allChildData = lists.map((list) => list.items).flat(1) 33 | if (allChildData[allChildData.length - 1].key === '') allChildData.pop() 34 | return allChildData as ChildData[] 35 | }, [lists]) 36 | 37 | // 对数组json专用的 列表选择特性 38 | const [currentItem, setCurrentItem] = useState(0) 39 | 40 | const handleSelectable = (selectedItems: React.Component[]) => { 41 | const ids: number[] = selectedItems.map((v) => { 42 | return v.props.id 43 | }) 44 | if (ids.length > 0) { 45 | setCurrentItem(ids[0]) 46 | } 47 | } 48 | 49 | const getSubField = useSubFieldQuery(data, access, fieldInfo, fatherInfo, viewport) 50 | 51 | return ( 52 | 53 | 90 | 91 | {data.length > 0 ? getSubField(currentItem.toString(), 0) : null} 92 | 93 | 94 | ) 95 | } 96 | 97 | export const ArrayListViewEdition = (props: EditionProps) => { 98 | const { 99 | field, 100 | fieldInfo: { ctx } 101 | } = props 102 | 103 | // 该 list 组件只允许在根节点使用,如果不是根节点,通过 ctx 使用默认组件显示 104 | const DefaultEdition = ctx.getComponent(null, ['edition', 'array']) 105 | return field === undefined ? : 106 | } 107 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/ComponentMap.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react' 2 | import { ContainerProps, EditionProps, MenuActionProps } from './type/props' 3 | import { JSONSchema } from '../../type/Schema' 4 | import merge from 'lodash/merge' 5 | 6 | export type CpuEditionType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' | 'enum' | 'const' 7 | /** 8 | * 编辑器使用的所有组件的 map。 9 | * 10 | * 根据对应的组件角色索引到对应使用的组件。 11 | */ 12 | export interface IComponentMap { 13 | containerNormal: ComponentType 14 | containerShort: ComponentType 15 | title: ComponentType 16 | menuAction: ComponentType 17 | operation: Record> 18 | format: Record> 19 | edition: Record> 20 | drawer: ComponentType 21 | schemaErrorLogger: ComponentType 22 | } 23 | 24 | type PartialIComponentMap = Partial< 25 | { 26 | operation: Partial>> 27 | format: Partial>> 28 | edition: Partial>> 29 | } & Omit 30 | > 31 | 32 | export interface IViewsMap extends PartialIComponentMap { 33 | /** 34 | * 使用该自定义 view 的字段是否可以作为 [短字段](https://github.com/FurtherBank/json-schemaeditor-antd#短字段)。 35 | * 36 | * 注意,viewMap 中的所有组件都使用`shortable`的统一设置。 37 | * 38 | * 如果您需要对不同的组件设置不同的`shortable`值,可以使用不同的`viewType` 39 | */ 40 | shortable: boolean 41 | /** 42 | * 使用该自定义 view 的字段参数的 schema。 43 | * 44 | * 如果不设置,将认为该自定义 view 没有任何参数。 45 | * 46 | * 注:该字段仅供对外声明使用,为提高性能,并不对传入的参数进行校验。 47 | */ 48 | paramSchema?: JSONSchema 49 | } 50 | 51 | /** 52 | * 将合并 componentMap 和 viewsMap 的函数放在这个单例之中 53 | */ 54 | export class ComponentMap { 55 | /** 56 | * 合并 ComponentMap 或 ViewsMap 57 | * @param maps 58 | * @returns 59 | */ 60 | static merge(...maps: T[]) { 61 | return maps.reduce((resultMap, newMap) => { 62 | return merge(resultMap, newMap) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useArrayCreator.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { useCallback } from 'react' 3 | import CpuEditorContext from '../../../context' 4 | import { MergedSchema } from '../../../context/mergeSchema' 5 | import { getDefaultValue } from '../../../definition/defaultValue' 6 | import { addRef } from '../../../utils' 7 | 8 | /** 9 | * [业务]返回 array 创建新项的函数,调用后会直接在 array 后面创建正确的新数组项内容。 10 | * @param ctx 11 | * @param data 12 | * @param access 13 | * @param arraySchema 14 | * @returns 15 | */ 16 | export const useArrayCreator = ( 17 | ctx: CpuEditorContext, 18 | data: any[], 19 | access: string[], 20 | arraySchema: MergedSchema | false | undefined, 21 | schemaEntry: string | undefined 22 | ) => { 23 | return useCallback(() => { 24 | const { maxItems, items, prefixItems } = arraySchema || {} 25 | // 数组新变量创建。注意如果使用已有变量直接创建时不要忘记深拷贝! 26 | const nowLength = data.length 27 | if (!maxItems || nowLength < maxItems) { 28 | if (prefixItems) { 29 | // 存在前缀约束的情况 30 | const { length: prefixLength, ref } = prefixItems 31 | if (nowLength < prefixLength) { 32 | // 新项处于 prefixItems 约束的位置时,通过对应 items 约束得到 defaultValue 33 | const defaultValue = getDefaultValue(ctx, addRef(ref, nowLength.toString())) 34 | ctx.executeAction('create', { schemaEntry, route: access, field: nowLength.toString(), value: defaultValue }) 35 | } else if (nowLength === prefixLength && items) { 36 | // 如果新建项恰好不属于 prefixItems,而且 additional 允许建且有约束,使用这个约束 37 | const defaultValue = getDefaultValue(ctx, items) 38 | ctx.executeAction('create', { schemaEntry, route: access, field: nowLength.toString(), value: defaultValue }) 39 | } 40 | } else if (data.length > 0) { 41 | // 此外如果有上一项(默认符合 schema),copy 上一项 42 | ctx.executeAction('create', { 43 | schemaEntry, 44 | route: access, 45 | field: nowLength.toString(), 46 | value: _.cloneDeep(data[data.length - 1]) 47 | }) 48 | } else { 49 | // 有 items 约束,使用 items 默认值,否则 null 50 | ctx.executeAction('create', { 51 | schemaEntry, 52 | route: access, 53 | field: nowLength.toString(), 54 | value: items ? getDefaultValue(ctx, items) : null 55 | }) 56 | } 57 | } 58 | }, [data, arraySchema, access, ctx]) 59 | } 60 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useArrayListContent.tsx: -------------------------------------------------------------------------------- 1 | import range from 'lodash/range' 2 | import { useMemo } from 'react' 3 | import { isShort } from '../../../context/virtual' 4 | import { canFieldCreate } from '../../../definition/schema' 5 | import { IField } from '../../../Field' 6 | import { addRef } from '../../../utils' 7 | import { ChildData, EmptyChildData, FieldDisplayList } from '../type/list' 8 | 9 | /** 10 | * [业务] 给出数组数据的两表渲染参数。 11 | * 12 | * 注: 13 | * 14 | * - 该函数输出短等级不一致的,最多两个列表。 15 | * - 如果可以创建新的列表项,最后一个列表的最后一项为`null` 16 | * @param data 17 | * @param schemaEntry 18 | * @param fieldInfo 19 | * @returns 20 | */ 21 | export const useArrayListContent = ( 22 | data: any[], 23 | schemaEntry: string | undefined, 24 | fieldInfo: IField 25 | ): FieldDisplayList[] => { 26 | const { valueEntry, mergedValueSchema, ctx } = fieldInfo 27 | const { prefixItems: { length: prefixLength = undefined, ref: prefixRef = '' } = {}, items } = mergedValueSchema || {} 28 | 29 | return useMemo(() => { 30 | const prefixList: ChildData[] = [] 31 | const itemsList: (ChildData | EmptyChildData)[] = [] 32 | 33 | // 分别确定两个表的短字段等级 34 | const prefixIsShort = prefixLength 35 | ? Math.min( 36 | ...range(prefixLength).map((i) => { 37 | const { [isShort]: shortable, title } = ctx.getMergedSchema(addRef(prefixRef, i.toString())) || {} 38 | return shortable ? (title ? 1 : 2) : 0 39 | }) 40 | ) 41 | : 0 42 | 43 | const { [isShort]: itemsShortable = false, title = undefined } = items ? ctx.getMergedSchema(items) || {} : {} 44 | 45 | const listShortLevel = [prefixIsShort, itemsShortable ? (title ? 1 : 2) : 0] 46 | 47 | // 如果前缀和余项的短字段等级不同,按照项是否为前缀项,分到两个表中 48 | if (listShortLevel[0] !== listShortLevel[1] && prefixLength !== undefined) { 49 | const prefixListLength = Math.min(prefixLength, data.length) 50 | for (let i = 0; i < prefixListLength; i++) { 51 | prefixList.push({ 52 | key: i.toString(), 53 | value: data[i] 54 | }) 55 | } 56 | } 57 | for (let i = prefixLength ?? 0; i < data.length; i++) { 58 | itemsList.push({ 59 | key: i.toString(), 60 | value: data[i] 61 | }) 62 | } 63 | 64 | // 如果可以创建新项,在第二个表后面加入 { key: '' },代表该项为 create 组件 65 | const canCreate = canFieldCreate(data, fieldInfo) 66 | if (canCreate) itemsList.push({ key: '' }) 67 | 68 | return [prefixList, itemsList] 69 | .map((propList, i) => ({ 70 | short: listShortLevel[i], 71 | items: propList 72 | })) 73 | .filter((list) => list.items.length > 0) 74 | }, [schemaEntry, valueEntry, data, ctx]) 75 | } 76 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useFatherInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { MergedSchema } from '../../../context/mergeSchema' 3 | import { jsonDataType } from '../../../utils' 4 | import { FatherInfo } from '../type/list' 5 | 6 | /** 7 | * [业务]获取到数组/对象数据的 fatherInfo 8 | * @param data 9 | * @param schemaEntry 10 | * @param valueEntry 11 | * @param mergedValueSchema 12 | * @returns 13 | */ 14 | export const useFatherInfo = ( 15 | data: Record | any[], 16 | schemaEntry: string | undefined, 17 | valueEntry: string | undefined, 18 | mergedValueSchema: MergedSchema | false | undefined 19 | ) => { 20 | const dataType = jsonDataType(data) 21 | 22 | return useMemo((): FatherInfo => { 23 | const { required } = mergedValueSchema || {} 24 | const childFatherInfo: FatherInfo = { 25 | schemaEntry, 26 | valueEntry, 27 | type: dataType 28 | } 29 | switch (dataType) { 30 | case 'array': 31 | childFatherInfo.type = 'array' 32 | childFatherInfo.length = data.length 33 | break 34 | default: 35 | childFatherInfo.type = 'object' 36 | if (required) childFatherInfo.required = required 37 | break 38 | } 39 | return childFatherInfo 40 | }, [mergedValueSchema, valueEntry, schemaEntry, dataType]) 41 | } 42 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useFieldModel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { ShortLevel } from '../../../definition' 3 | import { IField } from '../../../Field' 4 | import { getFieldSchema } from '../../../utils/schemaWithRef' 5 | import { FatherInfo } from '../type/list' 6 | 7 | /** 8 | * [业务]返回一个函数,传入 key 和 shortLevel 可以取得 subField 的 Field 组件 9 | * @param data 10 | * @param access 11 | * @param fieldInfo 12 | * @param fatherInfo 13 | * @param viewport 14 | * @returns 15 | */ 16 | export const useFieldModel = ( 17 | data: Record | any[], 18 | access: string[], 19 | fieldInfo: IField, 20 | fatherInfo: FatherInfo, 21 | viewport: string 22 | ) => { 23 | const { ctx, mergedValueSchema, valueEntry } = fieldInfo 24 | return useCallback( 25 | (key: string, short: ShortLevel) => { 26 | const subEntry = getFieldSchema(data, valueEntry, mergedValueSchema, key) || undefined 27 | const Field = ctx.Field 28 | return ( 29 | 37 | ) 38 | }, 39 | [data, access, valueEntry, fieldInfo, fatherInfo, ctx] 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useMenuActionComponents.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ContainerProps } from '../type/props' 3 | import { getRefByOfChain } from '../../../context/ofInfo' 4 | import { getDefaultValue, defaultTypeValue } from '../../../definition/defaultValue' 5 | import { JsonTypes } from '../../../definition/reducer' 6 | import { jsonDataType } from '../../../utils' 7 | 8 | /** 9 | * [业务]通过 Field 的属性得到使用的 菜单栏 和 操作栏 组件 10 | * @param props 11 | */ 12 | export const useMenuActionComponents = (props: ContainerProps) => { 13 | const { data, route, field, schemaEntry, fieldInfo, availableMenuActions, menuActionHandlers } = props 14 | const { mergedValueSchema, ofOption, ctx } = fieldInfo 15 | 16 | const dataType = jsonDataType(data) 17 | const { const: constValue, enum: enumValue, type: allowedTypes } = mergedValueSchema || {} 18 | const valueType = constValue !== undefined ? 'const' : enumValue !== undefined ? 'enum' : dataType 19 | const directActionComs: JSX.Element[] = [] 20 | // a. 如果存在 oneOfOption,加入 oneOf 调整组件 21 | if (schemaEntry && ofOption !== null) { 22 | const ofInfo = ctx.getOfInfo(schemaEntry)! 23 | const OneOfOperation = ctx.getComponent(null, ['operation', 'oneOf']) 24 | directActionComs.push( 25 | { 29 | const schemaRef = getRefByOfChain(ctx, schemaEntry!, value) 30 | const defaultValue = getDefaultValue(ctx, schemaRef, data) 31 | ctx.executeAction('change', { route, field, value: defaultValue, schemaEntry }) 32 | }} 33 | key={'oneOf'} 34 | /> 35 | ) 36 | } 37 | 38 | // b. 如果不是 const/enum,且允许多种 type,加入 type 调整组件 39 | if ( 40 | valueType !== 'const' && 41 | valueType !== 'enum' && 42 | (mergedValueSchema === false || !allowedTypes || allowedTypes.length !== 1) 43 | ) { 44 | const typeOptions = allowedTypes && allowedTypes.length > 0 ? allowedTypes : JsonTypes 45 | const TypeOperation = ctx.getComponent(null, ['operation', 'type']) 46 | directActionComs.push( 47 | { 51 | ctx.executeAction('change', { route, field, value: defaultTypeValue[value], schemaEntry }) 52 | }} 53 | key={'type'} 54 | /> 55 | ) 56 | } 57 | // 4. 设置菜单动作栏组件 58 | const menuActionComs = availableMenuActions.map((actType) => { 59 | const MenuActionComponent = ctx.getComponent(null, ['menuAction']) 60 | return 61 | }) 62 | 63 | return [directActionComs, menuActionComs] as const 64 | } 65 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useMenuActionHandlers.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback } from 'react' 2 | import CpuEditorContext from '../../../context' 3 | import { getDefaultValue } from '../../../definition/defaultValue' 4 | import { FatherInfo } from '../type/list' 5 | 6 | /** 7 | * [业务] 获取所有 menuAction 的处理函数 8 | * @param ctx 9 | * @param route 10 | * @param field 11 | * @param valueEntry 12 | * @param data 13 | * @returns 14 | */ 15 | export const useMenuActionHandlers = ( 16 | ctx: CpuEditorContext, 17 | route: string[], 18 | field: string | undefined, 19 | fatherInfo: FatherInfo | undefined, 20 | schemaEntry: string | undefined, 21 | valueEntry: string | undefined, 22 | data: any 23 | ) => { 24 | const { schemaEntry: parentSchemaEntry } = fatherInfo ?? {} 25 | 26 | const resetHandler = useCallback(() => { 27 | ctx.executeAction('change', { route, field, value: getDefaultValue(ctx, valueEntry, data) }) 28 | }, [data, valueEntry, route, field, ctx]) 29 | 30 | const menuActionHandlers = useMemo( 31 | () => ({ 32 | detail: () => { 33 | ctx.interaction.setDrawer(route, field) 34 | }, 35 | reset: resetHandler, 36 | moveup: () => { 37 | ctx.executeAction('moveup', { route, field, schemaEntry: parentSchemaEntry }) 38 | }, 39 | movedown: () => { 40 | ctx.executeAction('movedown', { route, field, schemaEntry: parentSchemaEntry }) 41 | }, 42 | delete: () => { 43 | ctx.executeAction('delete', { route, field, schemaEntry: parentSchemaEntry }) 44 | }, 45 | undo: () => { 46 | ctx.executeAction('undo') 47 | }, 48 | redo: () => { 49 | ctx.executeAction('redo') 50 | }, 51 | paste: () => { 52 | ctx.executeAction('change', { route, field, value: 0, schemaEntry }) 53 | } 54 | }), 55 | [resetHandler, route, field, ctx, parentSchemaEntry] 56 | ) 57 | 58 | return menuActionHandlers 59 | } 60 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useObjectCreator.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import CpuEditorContext from '../../../context' 3 | import { MergedSchema } from '../../../context/mergeSchema' 4 | import { getDefaultValue } from '../../../definition/defaultValue' 5 | import { getValueByPattern } from '../../../utils' 6 | 7 | /** 8 | * [业务]返回 object 创建新项的函数,传入新属性名称调用,会直接创建正确的新属性内容。 9 | * @param ctx 10 | * @param data 11 | * @param access 12 | * @param schemaEntry 13 | * @param objectSchema 14 | * @returns `string`: 不能创建的原因; `CpuEditorAction`:代表正确创建,返回创建的`action` 15 | */ 16 | export const useObjectCreator = ( 17 | ctx: CpuEditorContext, 18 | data: Record, 19 | access: string[], 20 | schemaEntry: string | undefined, 21 | objectSchema: MergedSchema | false | undefined 22 | ) => { 23 | return useCallback( 24 | (newPropName: string) => { 25 | const { properties, patternProperties, additionalProperties } = objectSchema || {} 26 | let newValueEntry = undefined 27 | if (data[newPropName] !== undefined) { 28 | return `字段 ${newPropName} 已经存在!` 29 | } else { 30 | const newPropRef = 31 | (properties && properties[newPropName]) ?? 32 | (patternProperties && getValueByPattern(patternProperties, newPropName)) 33 | if (!newPropRef) { 34 | if (additionalProperties !== false) { 35 | newValueEntry = additionalProperties 36 | } else { 37 | return `${newPropName} 不匹配 properties 中的名称或 patternProperties 中的正则式` 38 | } 39 | } else { 40 | newValueEntry = newPropRef 41 | } 42 | } 43 | return ctx.executeAction('create', { 44 | route: access, 45 | field: newPropName, 46 | value: getDefaultValue(ctx, newValueEntry), 47 | schemaEntry 48 | }) 49 | }, 50 | [data, objectSchema, access, ctx] 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useObjectListContent.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { isShort } from '../../../context/virtual' 3 | import { ShortLevel } from '../../../definition' 4 | import { canFieldCreate } from '../../../definition/schema' 5 | import { IField } from '../../../Field' 6 | import { getValueByPattern } from '../../../utils' 7 | import { ChildData, EmptyChildData, FieldDisplayList } from '../type/list' 8 | 9 | /** 10 | * [业务] 给出对象数据的两表渲染参数。 11 | * 12 | * 注: 13 | * 14 | * - 该函数输出短等级不一致的,最多两个列表。 15 | * - 如果可以创建新的列表项,最后一个列表的最后一项为`null` 16 | * @param data 17 | * @param schemaEntry 18 | * @param fieldInfo 19 | * @returns 20 | */ 21 | export const useObjectListContent = ( 22 | data: Record, 23 | schemaEntry: string | undefined, 24 | fieldInfo: IField 25 | ): FieldDisplayList[] => { 26 | const { valueEntry, mergedValueSchema, ctx } = fieldInfo 27 | const { properties, additionalProperties, patternProperties } = mergedValueSchema || {} 28 | 29 | return useMemo(() => { 30 | const shortenProps: ChildData[] = [] 31 | const normalProps: (ChildData | EmptyChildData)[] = [] 32 | 33 | // 短表在前,长表在后 34 | const listShortLevel = [ShortLevel.short, ShortLevel.no] 35 | 36 | // 按照属性是否是短字段分到两个表中 37 | for (const key in data) { 38 | if (Object.prototype.hasOwnProperty.call(data, key)) { 39 | const value = data[key] 40 | const patternRef = patternProperties ? getValueByPattern(patternProperties, key) : undefined 41 | const propRealRef = 42 | properties && properties[key] ? properties[key] : patternRef ? patternRef : additionalProperties 43 | if (propRealRef) { 44 | const { [isShort]: shortable } = ctx.getMergedSchema(propRealRef) || {} 45 | if (shortable) { 46 | shortenProps.push({ 47 | key, 48 | value: data[key] 49 | }) 50 | continue 51 | } 52 | } 53 | normalProps.push({ key, value }) 54 | } 55 | } 56 | 57 | // 如果可以创建新项,在第二个表后面加入 null,代表该项为 create 组件 58 | const canCreate = canFieldCreate(data, fieldInfo) 59 | if (canCreate) normalProps.push({ key: '' }) 60 | 61 | return [shortenProps, normalProps] 62 | .map((propList, i) => ({ 63 | short: listShortLevel[i], 64 | items: propList 65 | })) 66 | .filter((list) => list.items.length > 0) 67 | }, [schemaEntry, valueEntry, data, ctx]) 68 | } 69 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/hooks/useSubFieldQuery.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { ShortLevel } from '../../../definition' 3 | import { IField } from '../../../Field' 4 | import { getFieldSchema } from '../../../utils/schemaWithRef' 5 | import { FatherInfo } from '../type/list' 6 | 7 | /** 8 | * [业务]返回一个函数,传入 key 和 shortLevel 可以取得 subField 的 Field 组件 9 | * @param data 10 | * @param access 11 | * @param fieldInfo 12 | * @param fatherInfo 13 | * @param viewport 14 | * @returns 15 | */ 16 | export const useSubFieldQuery = ( 17 | data: Record | any[], 18 | access: string[], 19 | fieldInfo: IField, 20 | fatherInfo: FatherInfo, 21 | viewport: string 22 | ) => { 23 | const { ctx, mergedValueSchema, valueEntry } = fieldInfo 24 | return useCallback( 25 | (key: string, short: ShortLevel) => { 26 | const subEntry = getFieldSchema(data, valueEntry, mergedValueSchema, key) || undefined 27 | const Field = ctx.Field 28 | return ( 29 | 37 | ) 38 | }, 39 | [data, access, valueEntry, fieldInfo, fatherInfo, ctx] 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/type/list.tsx: -------------------------------------------------------------------------------- 1 | import { ShortLevel } from '../../../definition' 2 | import { IField } from '../../../Field' 3 | 4 | /** 5 | * 原则上来自于父字段的信息,不具有子字段特异性 6 | */ 7 | export interface FatherInfo { 8 | type: string // 是父亲的实际类型,非要求类型 9 | length?: number // 如果是数组,给出长度 10 | required?: string[] // 如果是对象,给出 required 属性 11 | schemaEntry: string | undefined // 父亲的 schemaEntry 12 | valueEntry: string | undefined // 父亲的 schemaEntry 13 | } 14 | 15 | export interface ChildData { 16 | key: string 17 | value: any 18 | } 19 | 20 | /** 21 | * 空的数组/对象子项数据,用作 create 组件 22 | */ 23 | export interface EmptyChildData { 24 | key: string 25 | } 26 | 27 | export interface FieldDisplayList { 28 | short: ShortLevel 29 | items: (ChildData | EmptyChildData)[] 30 | } 31 | 32 | export interface ListDisplayPanelProps { 33 | viewport: string 34 | lists: FieldDisplayList[] 35 | fatherInfo: FatherInfo 36 | fieldInfo: IField 37 | data: any 38 | access: string[] 39 | } 40 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/components/core/type/props.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { FieldProps, IField } from '../../../Field' 3 | import { MenuActionType } from '../../../menu/MenuActions' 4 | 5 | export interface EditionProps extends FieldProps { 6 | children?: ReactNode 7 | fieldInfo: IField 8 | } 9 | 10 | export interface FormatEditionProps extends EditionProps { 11 | format: string 12 | } 13 | 14 | export type MenuActionHandlers = Record void> 15 | 16 | /** 17 | * 应用短优化的容器组件使用的属性。 18 | * 19 | * 相比普通容器组件,不会从属性中继承 菜单动作组件 和 选择操作组件。 20 | */ 21 | export interface ContainerProps extends EditionProps { 22 | fieldDomId: string 23 | availableMenuActions: MenuActionType[] 24 | menuActionHandlers: MenuActionHandlers 25 | titleComponent: ReactNode 26 | valueComponent: ReactNode 27 | } 28 | 29 | export interface MenuActionProps { 30 | opType: T 31 | opHandler: () => void 32 | } 33 | 34 | export interface EditorDrawerProps { 35 | visible: boolean 36 | children?: ReactNode 37 | onClose: () => void 38 | } 39 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/context/interaction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 负责管理用户与编辑器的交互 3 | */ 4 | export class CpuInteraction { 5 | constructor(public setDrawer: (route: string[], field: string | undefined) => void) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/context/mergeSchema.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import CpuEditorContext, { SchemaArrayRefInfo } from '.' 3 | import { JSONSchema } from '../type/Schema' 4 | import { addRef } from '../utils' 5 | import { virtualSchemaProps, isShort, schemaIsShort } from './virtual' 6 | 7 | type processedSchemaProps = { 8 | type?: string[] 9 | properties?: { [k: string]: string } 10 | patternProperties?: { [k: string]: string } 11 | dependencies?: { [k: string]: string | string[] } 12 | /** 13 | * `schema`处理为 ref,`schema[]`处理为`SchemaArrayRefInfo` 14 | * 15 | * 不允许处理为`false`,不存在为`undefined` 16 | */ 17 | prefixItems?: SchemaArrayRefInfo 18 | items?: string | false 19 | additionalProperties?: string | false 20 | oneOf?: SchemaArrayRefInfo 21 | anyOf?: SchemaArrayRefInfo 22 | } 23 | 24 | export type MergedSchemaWithoutVirtual = Omit & 25 | processedSchemaProps 26 | 27 | /** 28 | * 合并后的 schema 只涉及到了用到的这一层的信息,不对子层的信息进行进一步归纳。 29 | */ 30 | export type MergedSchema = MergedSchemaWithoutVirtual & virtualSchemaProps 31 | 32 | /** 33 | * 将 schemaMap 以合并的方式得到其中的参数 34 | * 1. first 还需要知道使用的 ref 在哪 35 | * 2. 对于对象或者数组的,值为 schema 的属性,需要一个子info来处理,也合到里面 36 | * @param ctx 37 | * @param map 38 | * @returns 39 | */ 40 | export const mergeSchemaMap = (ctx: CpuEditorContext, map: Map) => { 41 | const result = {} as MergedSchema 42 | for (const [ref, schema] of map) { 43 | if (typeof schema === 'object') { 44 | for (const key in schema) { 45 | if (Object.prototype.hasOwnProperty.call(schema, key)) { 46 | // acts different by key 47 | const value = schema[key as keyof JSONSchema] as any 48 | switch (key) { 49 | case 'type': 50 | // intersection 51 | const typeValue: string[] = value instanceof Array ? value : [value] 52 | result[key] = result[key] ? _.intersection(result[key], typeValue) : typeValue 53 | if (result[key]!.length === 0) return false 54 | break 55 | case 'properties': 56 | case 'patternProperties': 57 | // mergeRef 58 | if (!result[key]) result[key] = {} 59 | for (const propKey in value) { 60 | if (Object.prototype.hasOwnProperty.call(value, propKey) && !result[key]![propKey]) { 61 | const propRef = addRef(ref, key, propKey)! 62 | result[key]![propKey] = propRef 63 | } 64 | } 65 | break 66 | case 'required': 67 | // union 68 | result[key] = result[key] ? _.union(result[key], value) : value 69 | break 70 | case 'dependencies': 71 | // merge ref/array 72 | if (!result[key]) result[key] = {} 73 | for (const propKey in value) { 74 | if (Object.prototype.hasOwnProperty.call(value, propKey) && !result[key]![propKey]) { 75 | const depValue = value[propKey] 76 | if (depValue instanceof Array) { 77 | result[key]![propKey] = depValue 78 | } else { 79 | const propRef = addRef(ref, key, propKey)! 80 | result[key]![propKey] = propRef 81 | } 82 | } 83 | } 84 | break 85 | case 'prefixItems': 86 | // array to Refs 87 | result[key] = { 88 | length: value.length, 89 | ref: addRef(ref, key)! 90 | } 91 | break 92 | case 'items': 93 | // to item Ref/array to prefix Refs 94 | if (!result[key]) { 95 | if (value instanceof Array) { 96 | result['prefixItems'] = { 97 | length: value.length, 98 | ref: addRef(ref, key)! 99 | } 100 | } else { 101 | result[key] = value ? addRef(ref, key)! : false 102 | } 103 | } 104 | break 105 | case 'additionalItems': 106 | // 旧版本 additionalItems => items 107 | result['items'] = value ? addRef(ref, key)! : false 108 | break 109 | case 'additionalProperties': 110 | // toRef 111 | result[key] = value ? addRef(ref, key)! : false 112 | break 113 | case 'oneOf': 114 | case 'anyOf': 115 | // SchemaArrayRefInfo 116 | if (!result[key]) { 117 | result[key] = { 118 | length: value.length, 119 | ref: addRef(ref, key)! 120 | } 121 | } 122 | break 123 | default: 124 | // first: no ref involved 125 | if (!result[key as keyof MergedSchemaWithoutVirtual]) { 126 | result[key as keyof MergedSchemaWithoutVirtual] = value 127 | } 128 | break 129 | } 130 | } 131 | } 132 | } else if (schema === false) return false 133 | } 134 | // post-process: update virtual attributes 135 | result[isShort] = schemaIsShort(ctx, result) 136 | return result as MergedSchema 137 | } 138 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/context/ofInfo.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { toOfName } from '../definition' 3 | import { addRef, deepReplace } from '../utils' 4 | import CpuEditorContext from '.' 5 | import { shallowValidate } from '../definition/shallowValidate' 6 | import { JSONSchema } from '../type/Schema' 7 | 8 | export interface ofSchemaCache { 9 | ofRef: string 10 | ofLength: number 11 | subOfRefs: (undefined | string)[] 12 | options: any[] 13 | } 14 | 15 | /** 16 | * 验证数据符合 oneOf/anyOf 的哪一个选项 17 | * @param data 18 | * @param schemaEntry 19 | * @param ctx 20 | * @returns `null`为无 oneOf/anyOf,`false`为不符合任何选项,`string`为选项链 21 | */ 22 | export const getOfOption = (data: any, schemaEntry: string, ctx: CpuEditorContext): string | null | false => { 23 | const ofCacheValue = schemaEntry ? ctx.getOfInfo(schemaEntry) : null 24 | if (ofCacheValue) { 25 | const { subOfRefs, ofLength, ofRef } = ofCacheValue 26 | for (let i = 0; i < ofLength; i++) { 27 | const subOfRef = subOfRefs[i] 28 | if (typeof subOfRef === 'string') { 29 | // 展开的 validate 为 string,就是子 oneOf 的 ref 30 | const subOption = getOfOption(data, subOfRef, ctx) 31 | console.assert(subOption !== null) 32 | if (subOption) return `${i}-${subOption}` 33 | } else { 34 | const valid = shallowValidate(data, addRef(ofRef, i.toString())!, ctx) 35 | if (valid) return i.toString() 36 | } 37 | } 38 | return false 39 | } 40 | return null 41 | } 42 | 43 | /** 44 | * 通过 of 链找到 schema 经层层选择之后引用的 valueEntry 45 | * @param ctx 46 | * @param schemaEntry 47 | * @param ofChain 48 | */ 49 | export const getRefByOfChain = (ctx: CpuEditorContext, schemaEntry: string, ofChain: string) => { 50 | const ofSelection = ofChain.split('-') 51 | let entry = schemaEntry 52 | for (const opt of ofSelection) { 53 | const { ofRef } = ctx.getOfInfo(entry)! 54 | entry = addRef(ofRef, opt)! 55 | } 56 | return entry 57 | } 58 | 59 | /** 60 | * 对 `schemaEntry` 设置 ofInfo 61 | * @param ctx 62 | * @param schemaEntry 63 | * @param rootSchema 64 | * @param nowOfRefs 65 | * @returns 66 | */ 67 | export const setOfInfo = ( 68 | ctx: CpuEditorContext, 69 | schemaEntry: string, 70 | rootSchema: JSONSchema, 71 | nowOfRefs: string[] = [] 72 | ) => { 73 | const mergedSchema = ctx.getMergedSchema(schemaEntry) 74 | if (!mergedSchema) return null 75 | // todo: noAnyOfChoice 的情况下 76 | const SchemaArrayRefInfo = mergedSchema.oneOf || mergedSchema.anyOf 77 | if (!SchemaArrayRefInfo) return null 78 | const { ref: ofRef, length: ofLength } = SchemaArrayRefInfo 79 | // 设置 ofCache (use Entry map ,root) 80 | if (ofRef && nowOfRefs.includes(ofRef)) { 81 | console.error('你进行了oneOf/anyOf的循环引用,这会造成无限递归,危', nowOfRefs, ofRef) 82 | ctx.ofInfoMap.set(schemaEntry, null) 83 | return null 84 | } else if (ofRef) { 85 | nowOfRefs.push(ofRef) 86 | 87 | // 接下来得到每个选项的 ref 和树选择需要的选项 options 88 | const subOfRefs = [] as (undefined | string)[] 89 | const oneOfOptions = [] 90 | for (let i = 0; i < ofLength; i++) { 91 | const ref = addRef(ofRef, i.toString())! 92 | const optMergedSchema = ctx.getMergedSchema(ref) 93 | const name = optMergedSchema ? toOfName(optMergedSchema) : '' 94 | const optOption = { 95 | value: i.toString(), 96 | title: name ? name : `Option ${i + 1}` 97 | } as any 98 | const optCache = ctx.ofInfoMap.has(ref) ? ctx.ofInfoMap.get(ref) : setOfInfo(ctx, ref, rootSchema, nowOfRefs) 99 | if (optCache) { 100 | const { options } = optCache 101 | optOption.children = options.map((option) => { 102 | return deepReplace(_.cloneDeep(option), 'value', (prev) => { 103 | return `${i}-${prev}` 104 | }) 105 | }) 106 | optOption.disabled = true 107 | // 选项有子选项,将子选项ref给他 108 | subOfRefs.push(ref) 109 | } else { 110 | subOfRefs.push(undefined) 111 | } 112 | oneOfOptions.push(optOption) 113 | } 114 | 115 | const ofInfo = { 116 | subOfRefs, 117 | ofRef: ofRef, 118 | ofLength, 119 | options: oneOfOptions 120 | } 121 | ctx.ofInfoMap.set(schemaEntry, ofInfo) 122 | return ofInfo 123 | } else { 124 | ctx.ofInfoMap.set(schemaEntry, null) 125 | return null 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/context/virtual.ts: -------------------------------------------------------------------------------- 1 | import { MergedSchemaWithoutVirtual } from './mergeSchema' 2 | import { getFormatType } from '../definition/formats' 3 | import CpuEditorContext from '.' 4 | 5 | // symbols 6 | export const isShort = Symbol.for('short') 7 | 8 | // all virtual props 9 | export type virtualSchemaProps = { 10 | [isShort]: boolean 11 | } 12 | 13 | /** 14 | * 确定schema是否可以短优化。条件: 15 | * 1. 类型确定且为string/number/bool/null,且没有oneof 16 | * 2. 有enum 17 | * 注:由于目前该函数主要从 valueInfo 确定子属性时调用,所以不必缓存。 18 | * @param mergedSchema 19 | */ 20 | export const schemaIsShort = (ctx: CpuEditorContext, mergedSchema: MergedSchemaWithoutVirtual) => { 21 | const { const: constValue, enum: enumValue, type: allowedTypes, format, view: { type: viewType } = {} } = mergedSchema 22 | if (constValue !== undefined || enumValue) return true 23 | 24 | if (allowedTypes && allowedTypes.length === 1) { 25 | if (viewType) { 26 | const { shortable = false } = ctx.viewsMap[viewType] || {} 27 | return shortable 28 | } 29 | const type = allowedTypes[0] 30 | switch (type) { 31 | case 'string': 32 | if (format && getFormatType(format)) return false 33 | return true 34 | case 'number': 35 | case 'integer': 36 | case 'boolean': 37 | case 'null': 38 | return true 39 | default: 40 | return false 41 | } 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/ajvInstance.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-shadow */ 2 | import Ajv from 'ajv' 3 | import addFormats from 'ajv-formats' 4 | import draft6MetaSchema from 'ajv/dist/refs/json-schema-draft-06.json' 5 | 6 | const defaultAjvInstance = new Ajv({ 7 | allErrors: true 8 | }) // options can be passed, e.g. {allErrors: true} 9 | defaultAjvInstance.addMetaSchema(draft6MetaSchema) 10 | addFormats(defaultAjvInstance) 11 | 12 | // 添加base-64 format 13 | defaultAjvInstance.addFormat('data-url', /^data:([a-z]+\/[a-z0-9-+.]+)?;(?:name=(.*);)?base64,(.*)$/) 14 | 15 | // 添加color format 16 | defaultAjvInstance.addFormat( 17 | 'color', 18 | // eslint-disable-next-line max-len 19 | /^(#?([0-9A-Fa-f]{3}){1,2}\b|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|(rgb\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/ 20 | ) 21 | 22 | // 添加row format 23 | defaultAjvInstance.addFormat('row', /.*/) 24 | 25 | // 添加multiline format 26 | defaultAjvInstance.addFormat('multiline', /.*/) 27 | 28 | // 添加 view 关键字 29 | defaultAjvInstance.addKeyword({ 30 | keyword: 'view', 31 | type: 'object', 32 | metaSchema: { 33 | type: 'object', 34 | properties: { type: { type: 'string' }, param: {} }, 35 | required: ['type'], 36 | additionalProperties: false 37 | } 38 | }) 39 | 40 | export default defaultAjvInstance 41 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/defaultValue.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import _ from 'lodash' 3 | import CpuEditorContext from '../context' 4 | import { jsonDataType, getValueByPattern, addRef } from '../utils' 5 | 6 | // defaultValue 7 | export const defaultTypeValue: any = { 8 | string: '', 9 | number: 0, 10 | integer: 0, 11 | object: {}, 12 | array: [], 13 | null: null, 14 | boolean: false 15 | } 16 | 17 | /** 18 | * 通过一个schemaEntry 得到schema,确定其创建时默认对象。 19 | * 允许找不到schema的场合,且前后变量保持最大兼容 20 | * @param ctx 21 | * @param entry 目标位置的 valueEntry,切换 of 选项的情况下不同于目前数据的 valueEntry,切换 22 | * @param nowData 目前的数据 23 | * @returns 24 | */ 25 | export const getDefaultValue = (ctx: CpuEditorContext, entry: string | undefined, nowData: any = undefined): any => { 26 | const mergedSchema = ctx.getMergedSchema(entry) 27 | if (!entry || !mergedSchema) return null 28 | 29 | const { 30 | properties, 31 | patternProperties, 32 | required, 33 | default: defaultValue, 34 | const: constValue, 35 | enum: enumValue, 36 | type: allowedTypes, 37 | prefixItems, 38 | items 39 | } = mergedSchema 40 | const nowDataType = jsonDataType(nowData) 41 | // 0. 如果nowData是对象,且有属性列表,就先剪掉不在列表中的属性,然后进行合并 42 | if (nowDataType === 'object') { 43 | if (properties || patternProperties) { 44 | nowData = produce(nowData, (draft: any) => { 45 | for (const key of Object.keys(draft)) { 46 | if ((!properties || !properties[key]) && (!patternProperties || !getValueByPattern(patternProperties, key))) 47 | delete draft[key] 48 | } 49 | }) 50 | } 51 | } 52 | // 1. 优先返回规定的 default 字段值(注意深拷贝,否则会形成对象环!) 53 | if (defaultValue !== undefined) { 54 | const defaultType = jsonDataType(defaultValue) 55 | if (defaultType === 'object' && nowDataType === 'object') { 56 | // 特殊:如果默认是 object,会采取最大合并 57 | return Object.assign({}, nowData, _.cloneDeep(defaultValue)) 58 | } 59 | return _.cloneDeep(defaultValue) 60 | } else { 61 | // 2. 如果有 const/enum,采用其值 62 | if (constValue !== undefined) return _.cloneDeep(constValue) 63 | if (enumValue !== undefined) return _.cloneDeep(enumValue[0]) 64 | // 3. oneOf/anyOf 选择第0项的schema返回 65 | const ofInfo = ctx.getOfInfo(entry) 66 | if (ofInfo) { 67 | return getDefaultValue(ctx, addRef(ofInfo.ofRef, '0')!) 68 | } 69 | } 70 | // 4. 按照 schema 寻找答案 71 | if (allowedTypes && allowedTypes.length > 0) { 72 | const type = allowedTypes[0] 73 | switch (type) { 74 | case 'object': 75 | const result = jsonDataType(nowData) === 'object' ? _.clone(nowData) : {} 76 | 77 | if (required) { 78 | // 仅对 required 中的属性进行创建 79 | for (const name of required) { 80 | if (properties && properties[name]) { 81 | result[name] = getDefaultValue(ctx, properties[name]) 82 | } else { 83 | return result 84 | } 85 | } 86 | } 87 | 88 | return result 89 | case 'array': 90 | const arrayResult = jsonDataType(nowData) === 'array' && items ? _.clone(nowData) : [] 91 | // 如果 items 是 arrayRefInfo,那么就是有前缀,覆写前缀 92 | if (prefixItems) { 93 | const { ref, length } = prefixItems 94 | for (let i = 0; i < length; i++) { 95 | arrayResult[i] = getDefaultValue(ctx, addRef(ref, i.toString())) 96 | } 97 | } 98 | return arrayResult 99 | default: 100 | break 101 | } 102 | return _.cloneDeep(defaultTypeValue[type]) 103 | } else { 104 | return null 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/formats.ts: -------------------------------------------------------------------------------- 1 | const longFormats = ['row', 'uri', 'uri-reference'] 2 | const extraLongFormats = ['multiline'] 3 | 4 | /** 5 | * 格式按照组件需要空间的情况分为三种类型: 6 | * - `short`:字段短优化后的空间即可正常显示 7 | * - `long`:需要一行的空间才能正常显示,不支持短优化 8 | * - `extralong`:需要多行的空间才能正常显示,不支持短优化 9 | * @param format 10 | * @returns 11 | */ 12 | export const getFormatType = (format: string | undefined) => { 13 | if (extraLongFormats.includes(format!)) return 2 14 | if (longFormats.includes(format!)) return 1 15 | return 0 16 | } 17 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import CpuEditorContext from '../context' 3 | import { MergedSchema } from '../context/mergeSchema' 4 | import { getOfOption, getRefByOfChain } from '../context/ofInfo' 5 | import { jsonDataType } from '../utils' 6 | import { getFieldSchema } from '../utils/schemaWithRef' 7 | 8 | export enum ShortLevel { 9 | no, 10 | short, 11 | extra 12 | } 13 | 14 | /** 15 | * 确定一个数据的**常量名称**。 16 | * 定义具体详见 [常量名称](https://gitee.com/furtherbank/json-schemaeditor-antd#常量名称) 17 | * @param v 18 | * @returns 19 | */ 20 | export const toConstName = (v: any) => { 21 | const t = jsonDataType(v) 22 | switch (t) { 23 | case 'object': 24 | return v.hasOwnProperty('name') ? v.name.toString() : `Object[${Object.keys(v).length}]` 25 | case 'array': 26 | return `Array[${v.length}]` 27 | case 'null': 28 | return 'null' // 注意 null 没有 toString 29 | default: 30 | return v.toString() 31 | } 32 | } 33 | 34 | /** 35 | * 确定一个模式在 of 选项中展示的**模式名称**。 36 | * 定义具体详见 [模式名称](https://gitee.com/furtherbank/json-schemaeditor-antd#模式名称) 37 | * @param mergedSchema 处理合并后的模式 38 | * @returns 39 | */ 40 | export const toOfName = (mergedSchema: MergedSchema) => { 41 | const { title, type } = mergedSchema 42 | if (title) return title 43 | if (type && type.length === 1) return type[0] 44 | return '' 45 | } 46 | 47 | /** 48 | * 得到当前 schemaEntry 下的 valueEntry 和 ofOption 49 | * @param data 当前数据 50 | * @param schemaEntry 当前数据的 schemaEntry 51 | * @param ctx 编辑器上下文对象 52 | * @returns 53 | */ 54 | export const getValueEntry = (data: any, schemaEntry: string | undefined, ctx: CpuEditorContext) => { 55 | let valueEntry = undefined as undefined | string 56 | let ofOption: string | false | null = null 57 | if (schemaEntry) { 58 | // 确定 valueEntry 59 | ofOption = getOfOption(data, schemaEntry, ctx) 60 | valueEntry = 61 | ofOption === null ? schemaEntry : ofOption === false ? undefined : getRefByOfChain(ctx, schemaEntry, ofOption) 62 | } 63 | return { valueEntry, ofOption } 64 | } 65 | 66 | /** 67 | * 得到当前数据通过 instancePath 继续查找得到的 schemaEntry 68 | * @param data 当然数据 69 | * @param path 继续的数据路径 70 | * @param ctx 编辑器上下文对象 71 | * @param curEntry 当前的 valueEntry,从这里开始查找 72 | */ 73 | export const getSchemaEntryByPath = (data: any, path: string[], ctx: CpuEditorContext, curEntry = '#') => { 74 | let schemaEntry: string | undefined = curEntry 75 | let nowData = data 76 | while (path.length > 0) { 77 | const key = path.shift()! 78 | const { valueEntry } = getValueEntry(data, schemaEntry, ctx) 79 | if (valueEntry === undefined) return undefined 80 | const mergedValueSchema = ctx.getMergedSchema(valueEntry) 81 | console.assert(typeof nowData === 'object') 82 | nowData = nowData[key] 83 | // 如果 getFieldEntry 得到 false,那就是一个指向 false 的 ref,直接当作 undefined 84 | schemaEntry = getFieldSchema(nowData, valueEntry, mergedValueSchema, key) || undefined 85 | if (schemaEntry === undefined) return undefined 86 | } 87 | return schemaEntry 88 | } 89 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/schema.ts: -------------------------------------------------------------------------------- 1 | import { FatherInfo } from '../components/core/type/list' 2 | import { IField, FieldProps } from '../Field' 3 | import { jsonDataType, getKeyByPattern } from '../utils' 4 | 5 | /** 6 | * 通过 schema 判断当前 json 是否可以创建新的属性。 7 | * @param props 8 | * @param fieldInfo 9 | */ 10 | export const canFieldCreate = (data: any, fieldInfo: IField) => { 11 | const dataType = jsonDataType(data) 12 | const { mergedValueSchema } = fieldInfo 13 | 14 | if (!mergedValueSchema) return dataType === 'array' || dataType === 'object' 15 | const { maxProperties, properties, additionalProperties, patternProperties, maxItems, items, prefixItems } = 16 | mergedValueSchema 17 | let autoCompleteKeys: string[] = [] 18 | switch (dataType) { 19 | case 'object': 20 | /** 21 | * object可以创建新属性,需要关注的条件: 22 | * 1. patternProperties 不为空,我们默认 patternProperties 只要有可用的正则就肯定能再创建。 23 | * 2. additionalProperties 不为 false 24 | * 3. 不超过 maxLength 25 | */ 26 | const nowKeys = Object.keys(data) 27 | // 1. 长度验证 28 | if (maxProperties !== undefined && nowKeys.length >= maxProperties) return false 29 | // 收集 properties 中可以创建的+可自动补全的属性 30 | const restKeys = properties ? Object.keys(properties).filter((key) => !nowKeys.includes(key)) : [] 31 | // todo: 依据 dependencies 筛选可创建属性 32 | if (properties) autoCompleteKeys = autoCompleteKeys.concat(restKeys) 33 | // 2. additionalProperties 验证 34 | if (additionalProperties !== false) return autoCompleteKeys 35 | // 3. patternProperties 有键 36 | if (patternProperties && Object.keys(patternProperties).length > 0) return autoCompleteKeys 37 | // 4. 有无剩余键 38 | return restKeys.length > 0 ? autoCompleteKeys : false 39 | case 'array': 40 | const prefixLength = prefixItems && items === false ? prefixItems.length : +Infinity 41 | const maxLength = maxItems === undefined ? +Infinity : maxItems 42 | return (maxLength < prefixLength ? data.length < maxLength : data.length < prefixLength) ? [] : false 43 | default: 44 | return false 45 | } 46 | } 47 | 48 | /** 49 | * 通过 schema 判断该字段(来自一个对象)是否可以重新命名 50 | * @param props 51 | * @returns 返回字符串为不可命名(同时其也是字段名称),返回正则为命名范围,返回空串即可命名 52 | */ 53 | export const canFieldRename = (props: FieldProps, fieldInfo: IField) => { 54 | const { field, fatherInfo } = props 55 | const { ctx, mergedEntrySchema } = fieldInfo 56 | // 注意,一个模式的 title 看 entryMap,如果有of等不理他 57 | const { title } = mergedEntrySchema || {} 58 | 59 | if (field === undefined) { 60 | return title ? title : ' ' 61 | } 62 | // 不是根节点,不保证 FatherInfo 一定存在,因为可能有抽屉! 63 | const { valueEntry: fatherValueEntry, type: fatherType } = fatherInfo ?? {} 64 | const fatherMergedValueSchema = ctx.getMergedSchema(fatherValueEntry) 65 | if (fatherType === 'array') { 66 | return title ? title + ' ' + field : field 67 | } else if (!fatherMergedValueSchema) { 68 | return '' 69 | } else { 70 | const { properties, patternProperties } = fatherMergedValueSchema 71 | 72 | if (properties && properties[field]) return title ? title : field 73 | 74 | const pattern = patternProperties ? getKeyByPattern(patternProperties, field) : undefined 75 | 76 | if (pattern) return pattern 77 | 78 | return '' 79 | } 80 | } 81 | 82 | /** 83 | * 通过 schema 判断该字段是否可删除。可删除条件: 84 | * 1. `field === undefined` 意味着根字段,不可删除 85 | * 2. 如果父亲是数组,只要不在数组 prefixItems 里面即可删除 86 | * 3. 如果父亲是对象,只要不在 required 里面即可删除 87 | * 88 | * @param props 89 | * @param fieldInfo 90 | */ 91 | export const canFieldDelete = (props: FieldProps, fieldInfo: IField) => { 92 | const { fatherInfo, field } = props 93 | const { ctx } = fieldInfo 94 | 95 | if (field === undefined) return false 96 | if (fatherInfo) { 97 | const { valueEntry: fatherValueEntry } = fatherInfo 98 | switch (fatherInfo.type) { 99 | case 'array': 100 | const { prefixItems } = ctx.getMergedSchema(fatherValueEntry) || {} 101 | const index = parseInt(field) 102 | return prefixItems ? index >= prefixItems.length : true 103 | case 'object': 104 | const fatherMergedValueSchema = ctx.getMergedSchema(fatherValueEntry) 105 | if (fatherMergedValueSchema && fatherMergedValueSchema.required) { 106 | return !fatherMergedValueSchema.required.includes(field) 107 | } else { 108 | return true 109 | } 110 | default: 111 | console.error('意外的判断情况') 112 | return false 113 | } 114 | } 115 | return false 116 | } 117 | 118 | /** 119 | * 字段是否为 required 字段 120 | * @param field 121 | * @param fatherInfo 122 | * @returns 123 | */ 124 | export const isFieldRequired = (field: string | undefined, fatherInfo?: FatherInfo | undefined) => { 125 | if (!fatherInfo || !field) return false 126 | const { required } = fatherInfo 127 | return required instanceof Array && required.indexOf(field) > -1 128 | } 129 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/definition/shallowValidate.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual' 2 | import CpuEditorContext from '../context' 3 | import { jsonDataType, addRef } from '../utils' 4 | import ajvInstance from './ajvInstance' 5 | 6 | const formatSchemaSheet = (function () { 7 | const sheet: any = {} 8 | return (format: string) => { 9 | if (!sheet[format]) 10 | sheet[format] = { 11 | type: 'string', 12 | format 13 | } 14 | return sheet[format] 15 | } 16 | })() 17 | 18 | /** 19 | * 通过对应entry对数据进行浅验证。注意会无视entry的oneOf/anyOf信息。 20 | * 详情说明见 [浅验证](https://gitee.com/furtherbank/json-schemaeditor-antd#浅验证) 21 | * @param data json 数据 22 | * @param valueEntry 23 | * @param ctx 24 | * @param deep 是否深入对对象属性进行验证。递归验证子属性时为 false 25 | * @returns 26 | */ 27 | export const shallowValidate = ( 28 | data: any, 29 | valueEntry: string | undefined, 30 | ctx: CpuEditorContext, 31 | deep = true 32 | ): boolean => { 33 | const mergedSchema = ctx.getMergedSchema(valueEntry) 34 | if (!mergedSchema) return false 35 | const { 36 | const: constValue, 37 | enum: enumValue, 38 | type: allowedTypes, 39 | format, 40 | properties, 41 | items, 42 | prefixItems, 43 | required 44 | } = mergedSchema 45 | const dataType = jsonDataType(data) 46 | if (constValue !== undefined) { 47 | return isEqual(data, constValue) 48 | } else if (enumValue !== undefined) { 49 | return enumValue.findIndex((v) => isEqual(v, data)) > -1 50 | } else if ( 51 | allowedTypes && 52 | allowedTypes.length === 1 && 53 | (dataType === allowedTypes[0] || (allowedTypes[0] === 'integer' && Number.isInteger(data))) 54 | ) { 55 | // 类型相同,进行详细验证 56 | switch (allowedTypes[0]) { 57 | case 'object': 58 | if (deep) { 59 | if (required) { 60 | return required.every((key) => { 61 | if (data[key] === undefined) return false 62 | const propRef = properties ? properties[key] : undefined 63 | if (propRef) { 64 | return shallowValidate(data[key], propRef, ctx, false) 65 | } 66 | return true 67 | }) 68 | } else { 69 | return true 70 | } 71 | } 72 | return true 73 | case 'array': 74 | if (deep) { 75 | if (prefixItems) { 76 | const { length, ref } = prefixItems 77 | return data.every((value: any, i: number) => { 78 | return i >= length 79 | ? shallowValidate(value, ref || undefined, ctx, false) 80 | : shallowValidate(value, addRef(ref, i.toString())!, ctx, false) 81 | }) 82 | } else if (items) { 83 | return data.every((value: any) => { 84 | return shallowValidate(value, items, ctx, false) 85 | }) 86 | } else { 87 | return true 88 | } 89 | } 90 | return true 91 | case 'string': 92 | if (format) { 93 | return ajvInstance.validate(formatSchemaSheet(format), data) 94 | } 95 | return true 96 | default: 97 | return true 98 | } 99 | } else if (allowedTypes) { 100 | return false 101 | } 102 | return true 103 | } 104 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/ModalSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Select } from 'antd' 2 | import { cloneDeep } from 'lodash' 3 | import React, { useState } from 'react' 4 | import examples from './examples' 5 | import { metaSchema } from 'json-schemaeditor-antd' 6 | 7 | // 接下来是示例选择功能的定义 8 | const exampleJson = examples(metaSchema) 9 | const options = Object.keys(exampleJson).map((key) => { 10 | return { label: key, value: key } 11 | }) as unknown as { label: string; value: string }[] 12 | 13 | const ModalSelect = (props: { cb: (data: any, schema: any) => void; cancelCb: () => void; visible: boolean }) => { 14 | const { cb, cancelCb, visible } = props 15 | const [item, setItem] = useState('基础') 16 | 17 | return ( 18 | { 23 | const data = cloneDeep(exampleJson[item][0]), 24 | schema = cloneDeep(exampleJson[item][1]) 25 | cb(data, schema) 26 | }} 27 | onCancel={() => { 28 | cancelCb() 29 | }} 30 | visible={visible} 31 | > 32 | { 37 | setItem(value) 38 | }} 39 | filterOption={(input, option) => { 40 | return !!option && option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 41 | }} 42 | defaultValue={'基础'} 43 | options={options} 44 | style={{ width: '100%' }} 45 | /> 46 | 47 | ) 48 | } 49 | 50 | export default ModalSelect 51 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/basic-data/$schema.string-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": { 5 | "type": "string", 6 | "format": "row" 7 | }, 8 | "definitions": {} 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/basic-data/string-array.json: -------------------------------------------------------------------------------- 1 | [ 2 | "我是 Assistant,是一个大型的语言模型。", 3 | "我的工作是通过人工智能技术帮助人们回答问题。", 4 | "我可以回答很多种类的问题,例如问题可能涉及历史、科学、政治、娱乐等领域。", 5 | "我是由 OpenAI 训练而来,并且我的知识涵盖了截止到 2021 年的信息。" 6 | ] 7 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/examples.ts: -------------------------------------------------------------------------------- 1 | // json 配置 2 | import general from './integrate/general.json' 3 | import $general from './integrate/$schema.general.json' 4 | import $default from './integrate/$schema.default.json' 5 | import Default from './integrate/default.json' // 注意 小写 default 保留字 6 | 7 | import $dataFacility from './integrate/$schema.dataFacility.json' 8 | import dataFacility from './integrate/dataFacility.json' 9 | 10 | import $dataTechTree from './integrate/$schema.dataTechTree.json' 11 | import dataTechTree from './integrate/dataTechTree.json' 12 | 13 | import $items from './integrate/$schema.items.json' 14 | import items from './integrate/items.json' 15 | 16 | import $basic from './integrate/$schema.basic.json' 17 | import basic from './integrate/basic.json' 18 | 19 | import $simple from './integrate/$schema.simple.json' 20 | import simple from './integrate/simple.json' 21 | 22 | import $reducerTest from './integrate/$schema.reducerTest.json' 23 | import reducerTest from './integrate/reducerTest.json' 24 | 25 | import $eslint from './integrate/$schema.eslint.json' 26 | import eslint from './integrate/eslint.json' 27 | 28 | import $list from './integrate/$schema.test-list.json' 29 | import list from './integrate/test-list.json' 30 | 31 | import $alternative from './integrate/$schema.alternative.json' 32 | import alternative from './integrate/alternative.json' 33 | 34 | import stringArray from './basic-data/string-array.json' 35 | import $stringArray from './basic-data/$schema.string-array.json' 36 | 37 | import { JSONSchema } from '../type/Schema' 38 | 39 | export type TestExample = [any, JSONSchema] 40 | export type TestExamples = { 41 | [key: string]: TestExample 42 | } 43 | 44 | export default (metaSchema: any) => { 45 | return { 46 | 基础: [basic, $basic], 47 | 一系列测试: [general, $general], 48 | 小型示例: [Default, $default], 49 | 简单示例: [simple, $simple], 50 | 模式编辑: [$default, metaSchema], 51 | 元模式自编辑: [metaSchema, metaSchema], 52 | '《星际探索者》设施配置示例': [dataFacility, $dataFacility], 53 | '《星际探索者》设施配置模式编辑': [$dataFacility, metaSchema], 54 | '《星际探索者》科技树示例': [dataTechTree, $dataTechTree], 55 | '《星际探索者》科技树配置模式编辑': [$dataTechTree, metaSchema], 56 | 'RMMZ 物品数据示例': [items, $items], 57 | 'reducer 测试实例': [reducerTest, $reducerTest], 58 | 'eslint(draft7)': [eslint, $eslint], 59 | 'view: list': [list, $list], 60 | 替代法则测试: [alternative, $alternative], 61 | 'basic: string Array': [stringArray, $stringArray] 62 | } as TestExamples 63 | } 64 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/feature/of-first-choice.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback } from 'react'; 2 | 3 | /** 4 | * 节流:设置 func 在 interval 时间内只执行一次。 5 | * 函数会随依赖改变,且保证组件卸载等情况下,函数可以跟进执行最后一次。 6 | * @param func 7 | * @param interval 8 | * @param deps 9 | * @returns 10 | */ 11 | export const useCooldown = void>( 12 | func: T, 13 | interval = 1, 14 | deps: React.DependencyList, 15 | ): T => { 16 | let cd = 0; 17 | let newArgs = undefined as undefined | any[]; 18 | const rf = (...args: any[]) => { 19 | if (cd === 0) { 20 | func(...args); 21 | cd = setTimeout(() => { 22 | if (newArgs) func(...newArgs); 23 | newArgs = undefined; 24 | cd = 0; 25 | }, interval) as unknown as number; 26 | } else { 27 | newArgs = args; 28 | } 29 | }; 30 | // 在组件卸载时,清除计时器 31 | useEffect(() => { 32 | // 不加载,只清除 33 | return () => { 34 | if (cd) { 35 | clearTimeout(cd); 36 | if (newArgs) func(...newArgs); 37 | } 38 | }; 39 | }, deps); 40 | 41 | return useCallback(rf as T, deps); 42 | }; 43 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.Classes.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": { 5 | "anyOf": [ 6 | { 7 | "$ref": "#/definitions/Class" 8 | }, 9 | { 10 | "type": "null" 11 | } 12 | ] 13 | }, 14 | "definitions": { 15 | "Class": { 16 | "type": "object", 17 | "additionalProperties": false, 18 | "properties": { 19 | "id": { 20 | "type": "integer" 21 | }, 22 | "expParams": { 23 | "type": "array", 24 | "items": { 25 | "type": "integer" 26 | } 27 | }, 28 | "traits": { 29 | "type": "array", 30 | "items": { 31 | "$ref": "#/definitions/Trait" 32 | } 33 | }, 34 | "learnings": { 35 | "type": "array", 36 | "items": { 37 | "$ref": "#/definitions/Learning" 38 | } 39 | }, 40 | "name": { 41 | "$ref": "#/definitions/Name" 42 | }, 43 | "note": { 44 | "type": "string" 45 | }, 46 | "params": { 47 | "type": "array", 48 | "items": { 49 | "type": "array", 50 | "items": { 51 | "type": "integer" 52 | } 53 | } 54 | } 55 | }, 56 | "required": ["expParams", "id", "learnings", "name", "note", "params", "traits"], 57 | "title": "Class" 58 | }, 59 | "Learning": { 60 | "type": "object", 61 | "additionalProperties": false, 62 | "properties": { 63 | "level": { 64 | "type": "integer" 65 | }, 66 | "note": { 67 | "type": "string" 68 | }, 69 | "skillId": { 70 | "type": "integer" 71 | } 72 | }, 73 | "required": ["level", "note", "skillId"], 74 | "title": "Learning" 75 | }, 76 | "Trait": { 77 | "type": "object", 78 | "additionalProperties": false, 79 | "properties": { 80 | "code": { 81 | "type": "integer" 82 | }, 83 | "dataId": { 84 | "type": "integer" 85 | }, 86 | "value": { 87 | "type": "number" 88 | } 89 | }, 90 | "required": ["code", "dataId", "value"], 91 | "title": "Trait" 92 | }, 93 | "Name": { 94 | "type": "string", 95 | "enum": ["", "冒险者(地牢设计)", "系统管理员", "小爱丽丝"], 96 | "title": "Name" 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.alternative.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "string", 4 | "title": "alternative", 5 | "$ref": "#/definitions/a", 6 | "definitions": { 7 | "a": { 8 | "title": "#/definitions/a", 9 | "description": "#/definitions/a", 10 | "$ref": "#/definitions/multiline" 11 | }, 12 | "multiline": { 13 | "format": "multiline" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "string" 3 | } 4 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.dataFacility.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": { 5 | "$ref": "#/definitions/DataFacilityElement" 6 | }, 7 | "definitions": { 8 | "DataFacilityElement": { 9 | "type": "object", 10 | "additionalProperties": false, 11 | "properties": { 12 | "name": { 13 | "title": "名称", 14 | "description": "建筑设施的名称", 15 | "type": "string" 16 | }, 17 | "description": { 18 | "title": "介绍", 19 | "description": "建筑设施的描述", 20 | "type": "string", 21 | "format": "multiline" 22 | }, 23 | "consume": { 24 | "title": "能源消耗", 25 | "description": "建筑设施每级的能耗", 26 | "type": "integer" 27 | }, 28 | "price": { 29 | "title": "升级价格", 30 | "description": "在索引等级时升级的各种资源耗费", 31 | "type": "array", 32 | "items": { 33 | "type": "array", 34 | "items": [ 35 | { 36 | "title": "能源", 37 | "type": "integer" 38 | }, 39 | { 40 | "title": "晶钢", 41 | "type": "integer" 42 | }, 43 | { 44 | "title": "芯髓", 45 | "type": "integer" 46 | } 47 | ], 48 | "additionalItems": false 49 | } 50 | }, 51 | "capacity": { 52 | "title": "资源容量", 53 | "description": "在索引等级时升级的各种资源容量", 54 | "type": "array", 55 | "items": { 56 | "type": "array", 57 | "items": { 58 | "type": "integer" 59 | } 60 | } 61 | } 62 | }, 63 | "required": ["capacity", "consume", "description", "name", "price"], 64 | "title": "设施" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.dataSolar.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": { 5 | "$ref": "#/definitions/DataSolarElement" 6 | }, 7 | "definitions": { 8 | "DataSolarElement": { 9 | "type": "object", 10 | "additionalProperties": false, 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | }, 15 | "radius": { 16 | "type": "integer" 17 | }, 18 | "radiusType": { 19 | "type": "integer" 20 | }, 21 | "orbitalRadius": { 22 | "type": "number" 23 | }, 24 | "texture": { 25 | "type": "string" 26 | }, 27 | "ring": { 28 | "type": "string" 29 | }, 30 | "orbitalAngularVelocity": { 31 | "type": "number" 32 | }, 33 | "spinVelocity": { 34 | "type": "number" 35 | }, 36 | "abundance": { 37 | "type": "array", 38 | "items": { 39 | "type": "integer" 40 | } 41 | }, 42 | "hard": { 43 | "type": "array", 44 | "items": { 45 | "type": "integer" 46 | } 47 | } 48 | }, 49 | "required": [ 50 | "abundance", 51 | "hard", 52 | "name", 53 | "orbitalAngularVelocity", 54 | "orbitalRadius", 55 | "radius", 56 | "radiusType", 57 | "ring", 58 | "spinVelocity", 59 | "texture" 60 | ], 61 | "title": "DataSolarElement" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.dataTechTree.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": { 5 | "type": "array", 6 | "items": { 7 | "$ref": "#/definitions/DataTechTreeElement" 8 | } 9 | }, 10 | "definitions": { 11 | "DataTechTreeElement": { 12 | "type": "object", 13 | "additionalProperties": false, 14 | "properties": { 15 | "name": { 16 | "type": "string" 17 | }, 18 | "description": { 19 | "type": "string", 20 | "format": "multiline" 21 | }, 22 | "cost": { 23 | "type": "integer" 24 | } 25 | }, 26 | "required": ["cost", "description", "name"], 27 | "title": "DataTechTreeElement" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Default Schema", 3 | "description": "a simple object schema by default", 4 | "type": "object", 5 | "properties": { 6 | "key": { 7 | "type": "string", 8 | "format": "row" 9 | } 10 | }, 11 | "additionalProperties": false 12 | } 13 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.general.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "一系列测试", 4 | "properties": { 5 | "enumValue": { 6 | "enum": [ 7 | 15, 8 | "一段文字", 9 | false, 10 | [1, 2, 3, 4, 5], 11 | null, 12 | { 13 | "stuff": 123, 14 | "str": "一段文字" 15 | } 16 | ] 17 | }, 18 | "constValue": { 19 | "description": "这个变量的值应该是数字 123", 20 | "const": 123 21 | }, 22 | "typeError": { 23 | "description": "这个变量的类型应该是string,现在是object", 24 | "type": "string" 25 | }, 26 | "newNameTest": { 27 | "type": "object", 28 | "title": "变量创建-命名测试", 29 | "description": "模式中对 properties, patternProperties, additionalProperties 都有定义,在这里测试新创建的属性名能否正确对应到相应模式,以及重命名空间是否正确。", 30 | "properties": { 31 | "pro1": { 32 | "type": "integer", 33 | "default": 1 34 | }, 35 | "pro2": { 36 | "type": "integer", 37 | "default": 2 38 | }, 39 | "pro3": { 40 | "type": "integer", 41 | "default": 3 42 | }, 43 | "pro4": { 44 | "type": "integer", 45 | "default": 4 46 | }, 47 | "pro5": { 48 | "type": "integer", 49 | "default": 5 50 | }, 51 | "pro6": { 52 | "type": "integer", 53 | "default": 6 54 | } 55 | }, 56 | "required": ["pro1", "pro2"], 57 | "patternProperties": { 58 | "pattern[0-9]+": { 59 | "description": "这个属性改名应该满足表达式 pattern[0-9]+", 60 | "type": "number", 61 | "default": 19 62 | } 63 | }, 64 | "additionalProperties": { 65 | "type": "number", 66 | "description": "这是 additionalProperties,应当<50", 67 | "maximum": 50, 68 | "default": 49 69 | } 70 | }, 71 | "mess": { 72 | "$ref": "#/definitions/messDefForRefTest" 73 | }, 74 | "formats": { 75 | "type": "object", 76 | "title": "格式测试", 77 | "properties": { 78 | "color": { 79 | "type": "string", 80 | "format": "color" 81 | }, 82 | "date": { 83 | "type": "string", 84 | "format": "date" 85 | }, 86 | "email": { 87 | "type": "string", 88 | "format": "email" 89 | }, 90 | "ipv4": { 91 | "type": "string", 92 | "format": "ipv4" 93 | }, 94 | "row": { 95 | "type": "string", 96 | "format": "row" 97 | }, 98 | "multi": { 99 | "type": "string", 100 | "format": "multiline" 101 | }, 102 | "time": { 103 | "type": "string", 104 | "format": "time" 105 | }, 106 | "date-time": { 107 | "type": "string", 108 | "format": "date-time" 109 | } 110 | } 111 | }, 112 | "oneOfLayers": { 113 | "type": "array", 114 | "items": { 115 | "title": "oneOf套娃", 116 | "oneOf": [ 117 | { 118 | "title": "套娃", 119 | "oneOf": [ 120 | { 121 | "type": "object", 122 | "title": "类型为对象", 123 | "properties": { 124 | "type": { 125 | "enum": ["object", "array"] 126 | } 127 | }, 128 | "required": ["type"] 129 | }, 130 | { 131 | "type": "object", 132 | "title": "类型为其它", 133 | "properties": { 134 | "type": { 135 | "enum": ["string", "number", "boolean", "integer", "null"] 136 | } 137 | }, 138 | "required": ["type"] 139 | } 140 | ] 141 | }, 142 | { 143 | "title": "number[]", 144 | "type": "array", 145 | "items": { 146 | "type": "number" 147 | } 148 | }, 149 | { 150 | "title": "string[]", 151 | "type": "array", 152 | "items": { 153 | "type": "string" 154 | } 155 | }, 156 | { 157 | "title": "color", 158 | "type": "string", 159 | "format": "color" 160 | }, 161 | { 162 | "title": "email", 163 | "type": "string", 164 | "format": "email" 165 | }, 166 | { 167 | "title": "date", 168 | "type": "string", 169 | "format": "date" 170 | } 171 | ] 172 | } 173 | }, 174 | "smellyAndLong": { 175 | "enum": [ 176 | "该项目是一个使用antd写的基于json-schema的json编辑器。 目前还在高速开发迭代中……\n该项目具有简单、可靠等特点。", 177 | "1919810", 178 | 114514 179 | ], 180 | "title": "又臭又长", 181 | "description": "这是一个又臭又长的枚举字符串。" 182 | } 183 | }, 184 | "required": ["enumValue", "constValue", "typeError", "newNameTest", "formats"], 185 | "additionalProperties": false, 186 | "definitions": { 187 | "messDefForRefTest": { 188 | "title": "混乱", 189 | "description": "这个属性的模式定义是混乱的,意在这种情况下看组件是否正常工作。", 190 | "type": ["object", "array", "string", "number", "integer"], 191 | "properties": { 192 | "num": { 193 | "type": "number" 194 | } 195 | }, 196 | "additionalProperties": { 197 | "type": "number" 198 | }, 199 | "required": ["num"], 200 | "items": { 201 | "type": "string", 202 | "format": "color" 203 | }, 204 | "format": "color", 205 | "maxLength": 5, 206 | "minimum": 20, 207 | "multipleOf": 2 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.items.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "items": [ 5 | { 6 | "type": "null" 7 | } 8 | ], 9 | "additionalItems": { 10 | "$ref": "#/definitions/Item" 11 | }, 12 | "definitions": { 13 | "Item": { 14 | "type": "object", 15 | "additionalProperties": false, 16 | "properties": { 17 | "id": { 18 | "type": "integer" 19 | }, 20 | "animationId": { 21 | "title": "动画 id", 22 | "type": "integer" 23 | }, 24 | "consumable": { 25 | "title": "是否消耗", 26 | "type": "boolean" 27 | }, 28 | "damage": { 29 | "$ref": "#/definitions/Damage" 30 | }, 31 | "description": { 32 | "title": "介绍", 33 | "type": "string", 34 | "format": "multiline" 35 | }, 36 | "effects": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/Effect" 40 | } 41 | }, 42 | "hitType": { 43 | "title": "命中类型", 44 | "type": "integer" 45 | }, 46 | "iconIndex": { 47 | "title": "图标索引", 48 | "type": "integer" 49 | }, 50 | "itypeId": { 51 | "title": "物品类型", 52 | "type": "integer" 53 | }, 54 | "name": { 55 | "title": "名称", 56 | "type": "string" 57 | }, 58 | "note": { 59 | "title": "备注", 60 | "type": "string", 61 | "format": "multiline" 62 | }, 63 | "occasion": { 64 | "title": "使用场合", 65 | "type": "integer" 66 | }, 67 | "price": { 68 | "title": "价格", 69 | "type": "integer" 70 | }, 71 | "repeats": { 72 | "title": "作用次数", 73 | "type": "integer" 74 | }, 75 | "scope": { 76 | "title": "使用范围", 77 | "type": "integer" 78 | }, 79 | "speed": { 80 | "title": "速度修正", 81 | "type": "integer" 82 | }, 83 | "successRate": { 84 | "title": "成功率", 85 | "type": "integer" 86 | }, 87 | "tpGain": { 88 | "title": "获得TP", 89 | "type": "integer" 90 | } 91 | }, 92 | "required": [ 93 | "animationId", 94 | "consumable", 95 | "damage", 96 | "description", 97 | "effects", 98 | "hitType", 99 | "iconIndex", 100 | "id", 101 | "itypeId", 102 | "name", 103 | "note", 104 | "occasion", 105 | "price", 106 | "repeats", 107 | "scope", 108 | "speed", 109 | "successRate", 110 | "tpGain" 111 | ], 112 | "title": "物品" 113 | }, 114 | "Damage": { 115 | "type": "object", 116 | "additionalProperties": false, 117 | "properties": { 118 | "critical": { 119 | "title": "可否暴击", 120 | "type": "boolean" 121 | }, 122 | "elementId": { 123 | "title": "属性", 124 | "type": "integer" 125 | }, 126 | "formula": { 127 | "title": "伤害公式", 128 | "type": "string" 129 | }, 130 | "type": { 131 | "title": "伤害类型", 132 | "type": "integer" 133 | }, 134 | "variance": { 135 | "title": "分散度", 136 | "type": "integer" 137 | } 138 | }, 139 | "required": ["critical", "elementId", "formula", "type", "variance"], 140 | "title": "伤害" 141 | }, 142 | "Effect": { 143 | "type": "object", 144 | "additionalProperties": false, 145 | "properties": { 146 | "code": { 147 | "type": "integer" 148 | }, 149 | "dataId": { 150 | "type": "integer" 151 | }, 152 | "value1": { 153 | "type": "number" 154 | }, 155 | "value2": { 156 | "type": "integer" 157 | } 158 | }, 159 | "required": ["code", "dataId", "value1", "value2"], 160 | "title": "特殊效果" 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.reducerTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/ReducerTest", 4 | "definitions": { 5 | "ReducerTest": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "arr": { 10 | "type": "array", 11 | "items": { 12 | "type": "integer" 13 | } 14 | }, 15 | "obj": { 16 | "$ref": "#/definitions/Obj" 17 | }, 18 | "needToRename": { 19 | "$ref": "#/definitions/NeedToRename" 20 | }, 21 | "moveUpAndDown": { 22 | "type": "array", 23 | "items": { 24 | "$ref": "#/definitions/MoveUpAndDownElement" 25 | } 26 | } 27 | }, 28 | "required": ["arr", "moveUpAndDown", "needToRename", "obj"], 29 | "title": "ReducerTest" 30 | }, 31 | "MoveUpAndDownClass": { 32 | "type": "object", 33 | "additionalProperties": false, 34 | "properties": { 35 | "key": { 36 | "type": "string" 37 | } 38 | }, 39 | "required": ["key"], 40 | "title": "MoveUpAndDownClass" 41 | }, 42 | "NeedToRename": { 43 | "type": "object", 44 | "additionalProperties": false, 45 | "properties": { 46 | "num": { 47 | "type": "integer" 48 | } 49 | }, 50 | "required": ["num"], 51 | "title": "NeedToRename" 52 | }, 53 | "Obj": { 54 | "type": "object", 55 | "additionalProperties": false, 56 | "properties": { 57 | "a": { 58 | "type": "string" 59 | }, 60 | "b": { 61 | "type": "boolean" 62 | }, 63 | "c": { 64 | "type": "null" 65 | } 66 | }, 67 | "required": ["a", "b", "c"], 68 | "title": "Obj" 69 | }, 70 | "MoveUpAndDownElement": { 71 | "anyOf": [ 72 | { 73 | "$ref": "#/definitions/MoveUpAndDownClass" 74 | }, 75 | { 76 | "type": "integer" 77 | } 78 | ], 79 | "title": "MoveUpAndDownElement" 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.sceneConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/SceneConfig", 4 | "definitions": { 5 | "SceneConfig": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "name": { 10 | "type": "string", 11 | "title": "场景名" 12 | }, 13 | "responseDelay": { 14 | "type": "integer", 15 | "title": "设置响应延迟(s)" 16 | }, 17 | "responseCode": { 18 | "type": "integer", 19 | "title": "响应状态码" 20 | }, 21 | "rewriteResponseHeaders": { 22 | "$ref": "#/definitions/RewriteResponseHeaders" 23 | }, 24 | "responseData": { 25 | "$ref": "#/definitions/ResponseData" 26 | } 27 | }, 28 | "required": ["name", "responseCode", "responseData", "responseDelay", "rewriteResponseHeaders"], 29 | "title": "场景" 30 | }, 31 | "ResponseData": { 32 | "type": "object", 33 | "title": "响应数据" 34 | }, 35 | "RewriteResponseHeaders": { 36 | "type": "object", 37 | "additionalProperties": false, 38 | "properties": { 39 | "cache-control": { 40 | "type": "string" 41 | } 42 | }, 43 | "required": ["cache-control"], 44 | "title": "覆盖响应头" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/Formdata", 4 | "definitions": { 5 | "Formdata": { 6 | "type": "object", 7 | "additionalProperties": false, 8 | "properties": { 9 | "array": { 10 | "type": "array", 11 | "items": { 12 | "type": "integer" 13 | } 14 | }, 15 | "string": { 16 | "type": "string" 17 | }, 18 | "object": { 19 | "$ref": "#/definitions/Object" 20 | } 21 | }, 22 | "required": ["array", "object", "string"], 23 | "title": "Formdata" 24 | }, 25 | "Object": { 26 | "type": "object", 27 | "additionalProperties": false, 28 | "properties": { 29 | "number": { 30 | "type": "integer" 31 | }, 32 | "boolean": { 33 | "type": "integer" 34 | }, 35 | "array": { 36 | "type": "array", 37 | "items": { 38 | "type": "string" 39 | } 40 | } 41 | }, 42 | "required": ["array", "boolean", "number"], 43 | "title": "Object" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/$schema.test-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "type": "array", 4 | "view": { 5 | "type": "list" 6 | }, 7 | "items": [ 8 | { 9 | "type": "null" 10 | }, 11 | { 12 | "type": "string" 13 | }, 14 | { 15 | "type": "object", 16 | "properties": { 17 | "name": { 18 | "type": "string" 19 | }, 20 | "description": { 21 | "type": "string", 22 | "format": "multiline" 23 | } 24 | } 25 | } 26 | ], 27 | "additionalItems": { 28 | "type": "integer" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/alternative.json: -------------------------------------------------------------------------------- 1 | "abcd" 2 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/basic.json: -------------------------------------------------------------------------------- 1 | "A basic string" 2 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/dataFacility.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "基地", 4 | "description": "基地是星系运行的中枢系统,提供基础资源存储容量。\n如果基地被破坏,星系所有设施都将停止运行。", 5 | "consume": 0, 6 | "price": [ 7 | [0, 0, 0], 8 | [0, 5000, 2000], 9 | [0, 10000, 8000] 10 | ], 11 | "capacity": [ 12 | [10000, 10000, 10000], 13 | [20000, 20000, 20000], 14 | [32000, 32000, 32000] 15 | ] 16 | }, 17 | { 18 | "name": "能源矿井", 19 | "description": "能源矿井可以开采星球能源,供星球各项设施支撑。\n生产力与星球能源丰富度有关。", 20 | "consume": 0, 21 | "price": [ 22 | [0, 800, 0], 23 | [0, 800, 200], 24 | [0, 800, 400] 25 | ], 26 | "capacity": [ 27 | [1200, 0, 0], 28 | [2400, 0, 0], 29 | [3600, 0, 0] 30 | ] 31 | }, 32 | { 33 | "name": "晶钢矿井", 34 | "description": "晶钢矿井消耗能量开采晶钢,供星系各工程的使用。\n生产力与星球晶钢丰富度有关。", 35 | "consume": 1, 36 | "price": [ 37 | [0, 800, 0], 38 | [0, 800, 200], 39 | [0, 800, 400] 40 | ], 41 | "capacity": [ 42 | [0, 1200, 0], 43 | [0, 2400, 0], 44 | [0, 3600, 0] 45 | ] 46 | }, 47 | { 48 | "name": "芯髓矿井", 49 | "description": "芯髓矿井消耗能量开采芯髓,供精密制造研究用途。\n生产力与星球芯髓丰富度有关。", 50 | "consume": 1, 51 | "price": [ 52 | [0, 800, 0], 53 | [0, 800, 200], 54 | [0, 800, 400] 55 | ], 56 | "capacity": [ 57 | [0, 0, 1200], 58 | [0, 0, 2400], 59 | [0, 0, 3600] 60 | ] 61 | }, 62 | { 63 | "name": "行星防御系统", 64 | "description": "建立在星系中的防护网。\n可以抵御小行星、外星人、三体水滴等外来威胁。", 65 | "consume": 2, 66 | "price": [[0, 8000, 3000]], 67 | "capacity": [[0, 0, 0]] 68 | }, 69 | { 70 | "name": "戴森球", 71 | "description": "戴森球可以包裹恒星并获取恒星提供的巨大能量。\n只能建造在恒星上。", 72 | "consume": 0, 73 | "price": [[0, 8000, 5000]], 74 | "capacity": [[12000, 0, 0]] 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/dataSolar.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "太阳", 4 | "radius": 696000, 5 | "radiusType": 4, 6 | 7 | "orbitalRadius": 0, 8 | 9 | "texture": "Sun-Mi", 10 | "ring": "", 11 | 12 | "orbitalAngularVelocity": 0, 13 | "spinVelocity": 0.5988, 14 | 15 | "abundance": [246, 0, 0], 16 | "hard": [0, 0, 0] 17 | }, 18 | 19 | { 20 | "name": "水星", 21 | "radius": 2440, 22 | "radiusType": 1, 23 | 24 | "orbitalRadius": 0.3871, 25 | 26 | "texture": "Mercury-Mi", 27 | "ring": "", 28 | 29 | "orbitalAngularVelocity": 4.09, 30 | "spinVelocity": 0.2557, 31 | 32 | "abundance": [12, 16, 24], 33 | "hard": [0, 0, 1] 34 | }, 35 | 36 | { 37 | "name": "金星", 38 | "radius": 6052, 39 | "radiusType": 2, 40 | 41 | "orbitalRadius": 0.723, 42 | 43 | "texture": "Venus-Mi", 44 | "ring": "", 45 | 46 | "orbitalAngularVelocity": 1.6, 47 | "spinVelocity": 0.0617, 48 | 49 | "abundance": [24, 28, 7], 50 | "hard": [1, 0, 0] 51 | }, 52 | 53 | { 54 | "name": "地球", 55 | "radius": 6378, 56 | "radiusType": 2, 57 | 58 | "orbitalRadius": 1, 59 | 60 | "texture": "Earth", 61 | "ring": "", 62 | 63 | "orbitalAngularVelocity": 0.98, 64 | "spinVelocity": 15.041, 65 | 66 | "abundance": [8, 10, 11], 67 | "hard": [2, 1, 0] 68 | }, 69 | 70 | { 71 | "name": "火星", 72 | "radius": 3397, 73 | "radiusType": 1, 74 | 75 | "orbitalRadius": 1.5237, 76 | 77 | "texture": "Mars-Mi", 78 | "ring": "", 79 | 80 | "orbitalAngularVelocity": 0.52, 81 | "spinVelocity": 14.6204, 82 | 83 | "abundance": [13, 26, 24], 84 | "hard": [0, 1, 0] 85 | }, 86 | 87 | { 88 | "name": "木星", 89 | "radius": 71492, 90 | "radiusType": 4, 91 | 92 | "orbitalRadius": 5.2026, 93 | 94 | "texture": "Jupiter-Mi", 95 | "ring": "", 96 | 97 | "orbitalAngularVelocity": 0.083, 98 | "spinVelocity": 36.272, 99 | 100 | "abundance": [37, 25, 7], 101 | "hard": [0, 0, 1] 102 | }, 103 | 104 | { 105 | "name": "土星", 106 | "radius": 60268, 107 | "radiusType": 4, 108 | 109 | "orbitalRadius": 9.5549, 110 | 111 | "texture": "Saturn", 112 | "ring": "ring", 113 | 114 | "orbitalAngularVelocity": 0.033, 115 | "spinVelocity": 33.7831, 116 | 117 | "abundance": [19, 38, 16], 118 | "hard": [0, 1, 0] 119 | }, 120 | 121 | { 122 | "name": "天王星", 123 | "radius": 25559, 124 | "radiusType": 4, 125 | 126 | "orbitalRadius": 19.2184, 127 | 128 | "texture": "Uranus-Mi", 129 | "ring": "", 130 | 131 | "orbitalAngularVelocity": 0.012, 132 | "spinVelocity": 20.8817, 133 | 134 | "abundance": [11, 12, 35], 135 | "hard": [0, 0, 0] 136 | }, 137 | 138 | { 139 | "name": "海王星", 140 | "radius": 24764, 141 | "radiusType": 4, 142 | 143 | "orbitalRadius": 30.1104, 144 | 145 | "texture": "Neptune-Mi", 146 | "ring": "", 147 | 148 | "orbitalAngularVelocity": 0.006, 149 | "spinVelocity": 22.3463, 150 | 151 | "abundance": [18, 27, 20], 152 | "hard": [0, 2, 0] 153 | } 154 | ] 155 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/dataTechTree.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "name": "能源产能提升", 5 | "description": "噢! 瞧瞧! 我的天哪!\n你什么时候拥有了这么出色的汽修天赋?\n你的天赋完美升级了你的能源开采机器,\n能源的开采效率更快了!\n效果:所有能源矿井的开采效率×1.2", 6 | "cost": 500 7 | }, 8 | { 9 | "name": "能源矿井拓展", 10 | "description": "想要开发大型行星?\n先看看你的汽修水平吧?\n不错,现在你的汽修水平让你的能源开采机器获得了新结构,允许提高能源矿井等级至 II 级。\n建立高等级的能源开采设施,可以允许在能源采集难度更大的星球进行资源开采工作。", 11 | "cost": 800 12 | }, 13 | { 14 | "name": "开采节能技术", 15 | "description": "促进传统产业转型升级,促进制造业高端化、智能化、绿色化、服务化,构建绿色制造体系,推进产品全生命周期绿色管理,不断优化工业产品结构。\n新型科技对你的矿物开采机器进行了优化,能够减少能耗啦!\n效果:耗能设施运行所消耗的能源降低到80%", 16 | "cost": 1200 17 | }, 18 | { 19 | "name": "戴森球计划", 20 | "description": "还有什么比直接利用恒星的核聚变能源更令人激动呢?\n一个高度发达的文明,必然有能力将恒星用一个巨大的球状结构包围起来,使得恒星的大部分辐射能量被截获,只有这样才可以长期支持这个文明,使其发展到足够的高度。", 21 | "cost": 3000 22 | }, 23 | { 24 | "name": "可持续发展", 25 | "description": "怪事,手上的这份《科技树加点指南》为什么格外不一样?\n多了一张可持续发展规划图,ohhhhhh!\n效果:矿物开采使星球资源不容易枯竭", 26 | "cost": 5000 27 | }, 28 | { 29 | "name": "能源产能革新", 30 | "description": "你的能源开采机器进一步得到了锤炼,开采的效率显著提升啦!\n效果:所有能源矿井的开采效率×1.6", 31 | "cost": 5000 32 | }, 33 | { 34 | "name": "能源矿井阵列", 35 | "description": "你的能源开采机器有了自己的思维,能够自动在巨型行星上开采能源进行结构的优化,能够对巨型行星进行开发!\n可以继续提高能源开采设备的规模,提高能源矿井等级至 III 级。\n建立高等级的能源开采设施,可以允许在能源采集难度更大的星球进行资源开采工作。", 36 | "cost": 1200 37 | }, 38 | { 39 | "name": "节能转运", 40 | "description": "面对艰难的跨星系运输,经过科学改造,转运资源的消耗降低了!\n效果:在不同星系之间进行的资源运输所消耗的全体资源×0.80", 41 | "cost": 1200 42 | } 43 | ], 44 | [ 45 | { 46 | "name": "晶钢产能提升", 47 | "description": "你的晶体钢开采机器得到了锤炼,开采的效率更快了\n效果:所有晶体钢矿井开采效率×1.2", 48 | "cost": 500 49 | }, 50 | { 51 | "name": "晶钢矿井拓展", 52 | "description": "增加可接入式晶体钢开采单元,可以拓展晶钢开采设施的规模,允许提高晶钢矿井等级至 III 级,提高晶钢产能。\n建立高等级的晶钢开采设施,可以允许在晶钢采集难度更大的星球进行资源开采工作。", 53 | "cost": 800 54 | }, 55 | { 56 | "name": "便携拆卸技术", 57 | "description": "工程设施便携式拆卸迁移技术,拥有宇宙摸鱼技术协会认证,采用框架解构策略迁移工程设施。\n据测算,相对常规工程设施迁移解决方案,该技术方案可以有效利用该建筑建造成本的80%。\n此外该方案固有支持设施非整体性迁移,兼具灵活性,有效降低工程设施星际迁移成本,提高摸鱼效率,是您最好的选择!", 58 | "cost": 1200 59 | }, 60 | { 61 | "name": "基地结构优化", 62 | "description": "在星际漂泊最怕的是家被砸没了,改进了星际探索时的基地结构,更加坚固耐砸了!\n效果:可以进行二级基地的升级,基地升级后能够增加矿物的仓储量", 63 | "cost": 3000 64 | }, 65 | { 66 | "name": "基地框架更新", 67 | "description": "虽然优化了基地的结构,但是还是被陨石砸坏了,经过漫长的摸鱼,终于对基地的框架再次进行了更新,更加结实耐砸了!\n效果:可以进行三级基地的升级,三级基地增加了矿物的仓储量", 68 | "cost": 5000 69 | }, 70 | { 71 | "name": "晶钢产能革新", 72 | "description": "坏起来了,《科技树加点指南》有这项吗?\n算了,先点了再说!\n效果:所有晶钢矿井的开采效率×1.6", 73 | "cost": 5000 74 | }, 75 | { 76 | "name": "晶钢矿井阵列", 77 | "description": "没有超级挖矿机,就没有钢产量”\n效果:晶钢矿井III;能够开采难度为高的巨型行星的晶钢矿脉", 78 | "cost": 1200 79 | } 80 | ], 81 | [ 82 | { 83 | "name": "芯髓产能提升", 84 | "description": "你的芯片髓核开采机器得到了锤炼,开采的效率更快了!\n效果:芯片髓核矿井的开采效率×1.2", 85 | "cost": 500 86 | }, 87 | { 88 | "name": "芯髓矿井拓展", 89 | "description": "在更多的星球上挖掘这种硬通货!\n建立高等级的芯髓开采设施,可以允许在芯髓采集难度更大的星球进行资源开采工作。", 90 | "cost": 800 91 | }, 92 | { 93 | "name": "维修保险", 94 | "description": "虽然设施经常被陨石砸坏,但是还是要努力在下次被砸之前维修成功。\n效果:损坏设施的维修花费减少至原来的40%", 95 | "cost": 1200 96 | }, 97 | { 98 | "name": "行星防御网络", 99 | "description": "为了生存在太空的艰难被砸环境和避免突发的星际土匪抢劫意外,行星防御网络准备就绪。\n效果:能够建造行星防御网络", 100 | "cost": 3000 101 | }, 102 | { 103 | "name": "宇宙探索规划", 104 | "description": "花更少的钱,探索更远的星系,是我们摸鱼的另一大宗旨,总之少花一点是一点。\n减少星际探索的资源需求增长。", 105 | "cost": 5000 106 | }, 107 | { 108 | "name": "芯髓产能革新", 109 | "description": "这本《科技树加点指南》怎么有这么多效率提升小技巧? 每天一个效率提升小技巧,摸鱼的时间就更多了!\n效果:所有芯片髓核矿井的开采效率×1.6", 110 | "cost": 5000 111 | }, 112 | { 113 | "name": "芯髓矿井阵列", 114 | "description": "你的芯髓开采机器有了自己的思维,能够自动为自己的结构更进行优化啦!\n建立高等级的芯髓开采设施,可以允许在芯髓采集难度更大的星球进行资源开采工作。", 115 | "cost": 1200 116 | } 117 | ] 118 | ] 119 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "该示例的模式是[模式编辑]的 data" 3 | } 4 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": ["standard"], 4 | "parserOptions": { "ecmaVersion": 5 }, 5 | "rules": { 6 | "semi": "off", 7 | "indent": [ 8 | "warn", 9 | 2, 10 | { 11 | "VariableDeclarator": { "var": 2 }, 12 | "SwitchCase": 1, 13 | "outerIIFEBody": 0 14 | } 15 | ], 16 | "space-before-function-paren": "off", 17 | "operator-linebreak": ["error", "before", { "overrides": { "=": "after" } }], 18 | "no-cond-assign": "off", 19 | "no-useless-escape": "off", 20 | "no-return-assign": "off", 21 | "one-var": "off", 22 | "no-control-regex": "off" 23 | }, 24 | "env": { 25 | "node": true, 26 | "browser": true, 27 | "amd": true, 28 | "jasmine": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "enumValue": [1, 2, 3, 4, 5], 3 | "constValue": { 4 | "num": 123, 5 | "str": "将变量改成数字123,应当验证成功" 6 | }, 7 | "typeError": { 8 | "num": 123, 9 | "str": "该变量应该是字符串" 10 | }, 11 | "newNameTest": { 12 | "pro1": 20, 13 | "pro2": 15, 14 | "pro3": 55, 15 | "balabala": 100, 16 | "pattern567": 45.254 17 | }, 18 | "mess": ["#0055ff", "#9955ffff"], 19 | "formats": { 20 | "row": "如果你不喜欢现在的生活,那么你快去考研吧!", 21 | "multi": "全面推进,统筹兼顾,综合治理,融入其中,贯穿始终,切实抓好,扎实推进,加快发展,持续增收,积极稳妥,狠抓落实,从严控制\n严格执行,坚决制止,明确职责,高举旗帜,坚定不移,牢牢把握,积极争取,深入开展,注重强化,规范程序,改进作风,积极发展\n努力建设,依法实行,良性互动,优势互补,率先发展,互惠互利,做深做细,做实做好,全面分析,全面贯彻,持续推进,日趋完善\n全面落实、全面实施,逐步扭转,基本形成,普遍增加,基本建立,更加完备,逐步完善,明显提高,逐渐好转,逐步形成,不断加强\n持续增效,巩固深化,大幅提高,显著改善,不断增强,比较圆满。", 22 | "color": "#bbbbbb", 23 | "email": "abcdef@163.com", 24 | "ipv4": "127.0.0.1", 25 | "date": "2022-03-14", 26 | "time": "01:02:03", 27 | "date-time": "2023-04-14 16:24:20" 28 | }, 29 | "oneOfLayers": [ 30 | [1, 2, 3, 4], 31 | ["a", "b", "c", "d"], 32 | { 33 | "type": "object" 34 | }, 35 | { 36 | "type": "array" 37 | }, 38 | { 39 | "type": "string" 40 | }, 41 | "#ffffff", 42 | "2022-03-14", 43 | null 44 | ], 45 | "smellyAndLong": "该项目是一个使用antd写的基于json-schema的json编辑器。 目前还在高速开发迭代中……\n该项目具有简单、可靠等特点。" 46 | } 47 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/reducerTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "arr": [1, 2, 3], 3 | "obj": { 4 | "a": "string", 5 | "b": false, 6 | "c": null 7 | }, 8 | "needToRename": { 9 | "num": 123 10 | }, 11 | "moveUpAndDown": [ 12 | 1, 13 | 2, 14 | 3, 15 | { 16 | "key": "value" 17 | }, 18 | 4, 19 | 5 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/sceneConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scene", 3 | "responseDelay": 500, 4 | "responseCode": 200, 5 | "rewriteResponseHeaders": { 6 | "cache-control": "no-cache" 7 | }, 8 | "responseData": {} 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "array": [1, 2, 3, 4], 3 | "string": "555sdw", 4 | "object": { 5 | "number": 5, 6 | "boolean": 4, 7 | "array": ["dd", "ee", "ff"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/demos/integrate/test-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | null, 3 | "abcd", 4 | { 5 | "name": "项目名称", 6 | "description": "对象的 constName,如果存在 name 属性为 string,默认为 name 属性值,否则为 Object[propertyLength]" 7 | }, 8 | 2, 9 | 3, 10 | 4, 11 | 5, 12 | 6 13 | ] 14 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/helper/validate-errors/processValidateErrors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorObject } from 'ajv' 2 | import { uri2strArray } from '../../utils/path/uri' 3 | 4 | /** 5 | * 将数组形式的 errors 转为 key => value 形式的 errors; 6 | * 7 | * 可以加快组件的查找效率。 8 | * @param prevErrors 9 | * @param newErrors 10 | * @param dataPath 11 | * @param schemaEntry 12 | */ 13 | export const processValidateErrors = ( 14 | prevErrors: Record, unknown>[]>, 15 | newErrors: ErrorObject, unknown>[] = [], 16 | dataPath: string[] = [], 17 | schemaEntry: string | undefined = undefined 18 | ) => { 19 | const prefixInstancePath = '/' + dataPath.join('/') 20 | // 将旧的,部分验证字段下的节点删除 21 | // todo: 查找过程 O(n),还有优化空间(想不出好办法,但是数据结构维护会变复杂) 22 | Object.keys(prevErrors).forEach((key) => { 23 | if (key.startsWith(prefixInstancePath)) { 24 | delete prevErrors[key] 25 | } 26 | }) 27 | // 将新的 errors 进行修正,然后写到树上 28 | // todo: 转义 29 | newErrors.forEach((error) => { 30 | // instancePath 31 | if (dataPath.length > 0) { 32 | error.instancePath = prefixInstancePath + error.instancePath 33 | } 34 | // schemaPath 35 | if (schemaEntry) { 36 | const schemaPrefixPath = uri2strArray(schemaEntry) 37 | const errorSchemaPath = uri2strArray(error.schemaPath) 38 | error.schemaPath = '#/' + schemaPrefixPath.join('/') + '/' + errorSchemaPath.join('/') 39 | } 40 | // 赋值到错误数组上 41 | if (prevErrors[error.instancePath] instanceof Array) { 42 | prevErrors[error.instancePath].push(error) 43 | } else { 44 | prevErrors[error.instancePath] = [error] 45 | } 46 | }) 47 | return prevErrors 48 | } 49 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: 预览 4 | --- 5 | 6 | ## JSON Schema Editor 7 | 8 | 9 | 10 | 好家伙,有谁能想到用 Markdown 写代码呢?是哪位工程师思路如此清奇? 11 | More skills for writing demo: https://d.umijs.org/guide/basic#write-component-demo 12 | 13 | 第一个问题:`@umijs/plugin-antd`对 dumi 的 md 编译的 jsx 不过 babel,没有按需引入优化。 14 | 15 | ## demo2 16 | 17 | 我是谁? 18 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react' 2 | import { Provider } from 'react-redux' 3 | import Field from './Field' 4 | import EditorDrawer from './EditorDrawer' 5 | import Ajv from 'ajv' 6 | 7 | import CpuEditorContext from './context' 8 | import defaultAjvInstance from './definition/ajvInstance' 9 | import { JSONSchema } from './type/Schema' 10 | import { IComponentMap, IViewsMap } from './components/core/ComponentMap' 11 | import { CpuInteraction } from './context/interaction' 12 | import { antdComponentMap, antdViewsMap } from './components/antd' 13 | 14 | export interface EditorProps { 15 | onChange?: (data: any) => void | null 16 | data?: any 17 | schema?: JSONSchema | boolean 18 | id?: string | undefined 19 | style?: CSSProperties 20 | componentMap?: IComponentMap 21 | viewsMap?: Record 22 | rootMenuItems?: JSX.Element[] 23 | options?: { 24 | ajvInstance?: Ajv 25 | } 26 | } 27 | 28 | export const InfoContext = React.createContext(null!) 29 | 30 | const emptyArray: never[] = [] 31 | 32 | const Editor = (props: EditorProps, ref: React.ForwardedRef) => { 33 | const { 34 | schema, 35 | data, 36 | onChange, 37 | id, 38 | viewsMap = antdViewsMap, 39 | componentMap = antdComponentMap, 40 | rootMenuItems, 41 | options: { ajvInstance = defaultAjvInstance } = {} 42 | } = props 43 | 44 | // 详细抽屉功能 45 | const drawerRef = useRef(null) as React.RefObject 46 | const setDrawer = useCallback( 47 | (...args: any[]) => { 48 | // console.log('setDrawer', drawerRef.current); 49 | if (drawerRef.current) drawerRef.current.setDrawer(...args) 50 | }, 51 | [drawerRef] 52 | ) 53 | 54 | const interaction = useMemo(() => { 55 | return new CpuInteraction(setDrawer) 56 | }, [setDrawer]) 57 | 58 | // 新建 ctx 59 | const ctx = useMemo(() => { 60 | return new CpuEditorContext(data, schema, ajvInstance, id, interaction, componentMap, viewsMap) 61 | }, [schema, interaction, componentMap, viewsMap]) 62 | 63 | // 给 store 订阅 change(做成 effect) 64 | useEffect(() => { 65 | const change = () => { 66 | const changedData = ctx.store.getState().present.data 67 | if (onChange && typeof onChange === 'function') { 68 | onChange(changedData) 69 | } 70 | } 71 | const unsubscribe = ctx.store.subscribe(change) 72 | return unsubscribe 73 | }, [onChange, ctx]) 74 | 75 | // 暴露一下 api 76 | useImperativeHandle( 77 | ref, 78 | () => { 79 | return ctx 80 | }, 81 | [ctx] 82 | ) 83 | 84 | // 如果 data 更新来自外部,通过 setData 与 store 同步 85 | const presentData = ctx.store.getState().present.data 86 | if (data !== presentData) { 87 | // console.log('检测到外部更新:', data, presentData); 88 | ctx.store.dispatch({ 89 | type: 'setData', 90 | value: data 91 | }) 92 | } 93 | 94 | const SchemaErrorLogger = ctx.getComponent(null, ['schemaErrorLogger']) 95 | return ( 96 | 97 | {ctx.schemaError ? : null} 98 | 99 | 100 | 101 | 102 | 103 | ) 104 | } 105 | 106 | export default React.memo(forwardRef(Editor)) 107 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/menu/MenuActions.ts: -------------------------------------------------------------------------------- 1 | import { ofSchemaCache } from '../context/ofInfo' 2 | 3 | export const MenuActions = ['undo', 'redo', 'detail', 'reset', 'moveup', 'movedown', 'delete'] as const 4 | 5 | export type MenuActionType = typeof MenuActions[number] 6 | 7 | export interface FieldOpParams { 8 | create: true | string[] 9 | oneOf: ofSchemaCache 10 | type: string[] 11 | } 12 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/type/DirectActions.ts: -------------------------------------------------------------------------------- 1 | import { ofSchemaCache } from '../context/ofInfo' 2 | 3 | export interface DirectActionParams { 4 | create: true | string[] 5 | oneOf: ofSchemaCache 6 | type: string[] 7 | } 8 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/type/Schema.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchema6 } from 'json-schema' 2 | 3 | /** 4 | * 编辑器适用的 json-schema type definition 5 | * - 修好了`type`为字符串枚举,而json的`type`类型都是`string`所以不可赋值的bug 6 | * - 该定义不包括`boolean`,如果需要就`|`一下 7 | */ 8 | export type JSONSchema = Omit & { 9 | type?: string | string[] | undefined 10 | view?: { 11 | param?: any 12 | type: string 13 | } 14 | // old schema props 15 | id?: string 16 | } 17 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/type/ValueTypeMapper.ts: -------------------------------------------------------------------------------- 1 | export interface ValueTypeMapper { 2 | [k: string]: any 3 | string: string 4 | boolean: boolean 5 | number: number 6 | array: any[] 7 | null: null 8 | } 9 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { uri2strArray } from './path/uri' 2 | 3 | export const KeywordTypes = { 4 | intersection: ['type'], 5 | merge: ['properties', 'patternProperties'] 6 | } as any 7 | 8 | export const getKeywordType = (keyword: string) => { 9 | for (const type in KeywordTypes) { 10 | if (KeywordTypes[type].includes(keyword)) return type 11 | } 12 | return 'first' 13 | } 14 | 15 | export const concatAccess = (route: string[], ...args: (string | null | undefined)[]) => { 16 | const filtered = args.filter((value) => typeof value === 'string' && value) as string[] 17 | return route.concat(filtered) 18 | } 19 | 20 | export const jsonDataType = (data: any) => { 21 | return data === null ? 'null' : data instanceof Array ? 'array' : typeof data 22 | } 23 | 24 | /** 25 | * 简单的迭代器转数组 26 | * @param it 迭代器 27 | * @returns 28 | */ 29 | export const iterToArray = (it: Iterable): T[] => { 30 | const r = [] 31 | for (const ele of it) { 32 | r.push(ele) 33 | } 34 | return r 35 | } 36 | 37 | /** 38 | * 给 uri 路径继续添加子路径。 39 | * 不提供子路径也可以当作去末尾 / 的格式化工具 40 | * @param ref uri 路径 41 | * @param path 需要添加的子路径 42 | * @returns 43 | */ 44 | export const addRef = (ref: string | undefined, ...path: string[]) => { 45 | if (ref === undefined) return undefined 46 | if (ref[ref.length - 1] === '/') ref = ref.substring(0, ref.length - 1) 47 | 48 | path.forEach((v) => { 49 | if (v) ref = ref + '/' + v 50 | }) 51 | return ref 52 | } 53 | 54 | /** 55 | * 通过 data access 得到 data 的 ref 字符串,用作 id 56 | * todo: 只要有需要对这个输出字符串分割的可能,必须要转义 57 | * @param access 58 | * @returns 59 | */ 60 | export const getAccessRef = (access: string[]) => { 61 | return access.join('.') 62 | } 63 | 64 | /** 65 | * 通过数组 path 得到对象树中对象的位置。 66 | * @param data 67 | * @param path 68 | * @returns 69 | */ 70 | export const deepGet = (data: any, path: string[]) => { 71 | let nowData = data 72 | for (const field of path) { 73 | if (typeof nowData !== 'object') return undefined 74 | nowData = nowData[field] 75 | } 76 | return nowData 77 | } 78 | 79 | /** 80 | * 设置对象树某一位置的值。 81 | * 如果途中遇到了已定义的非对象,会取消操作 82 | * @param obj 83 | * @param ref json-pointer string without # 84 | * @param value 85 | * @returns 86 | */ 87 | export const deepSet = (obj: any, ref: string, value: any) => { 88 | const path = uri2strArray(ref) 89 | const oriObj = obj 90 | path.every((v, i) => { 91 | if (i === path.length - 1) { 92 | obj[v] = value 93 | } else if (obj[v] === undefined || obj[v] === null) { 94 | obj[v] = {} 95 | obj = obj[v] 96 | } else if (typeof obj[v] === 'object' && !(obj[v] instanceof Array)) { 97 | obj = obj[v] 98 | } else { 99 | return false 100 | } 101 | return true 102 | }) 103 | return oriObj 104 | } 105 | 106 | /** 107 | * 深度递归收集一个对象某个键的所有属性 108 | * @param obj 109 | * @param key 110 | * @returns 111 | */ 112 | export const deepCollect = (obj: any, key: string): any[] => { 113 | let collection: any[] = [] 114 | // js 原型链安全问题 115 | for (const k in obj) { 116 | if (k === key) { 117 | collection.push(obj[k]) 118 | } else { 119 | if (obj[k] && typeof obj[k] === 'object') { 120 | collection = collection.concat(deepCollect(obj[k], key)) 121 | } 122 | } 123 | } 124 | return collection 125 | } 126 | 127 | /** 128 | * 深度递归替换一个对象某个键的所有属性。 129 | * 注意如果需要深拷贝请提前做。 130 | * @param obj 131 | * @param key 132 | * @param map 替换函数 133 | * @returns 134 | */ 135 | export const deepReplace = (obj: any, key: string, map: (value: any, key: any) => any) => { 136 | // js 原型链安全问题 137 | for (const k in obj) { 138 | if (k === key) { 139 | obj[k] = map(obj[k], k) 140 | // console.log('替换',obj[k]); 141 | } else { 142 | if (obj[k] && typeof obj[k] === 'object') obj[k] = deepReplace(obj[k], key, map) 143 | } 144 | } 145 | return obj 146 | } 147 | 148 | /** 149 | * 将对象或map的keys当作正则表达式来匹配 key,如果匹配到返回这个正则表达式 150 | * @param map 151 | * @param key 152 | * @returns 153 | */ 154 | export const getKeyByPattern = (map: Map | { [x: string]: any }, key: string) => { 155 | const keys = map instanceof Map ? map.keys() : Object.keys(map) 156 | 157 | for (const k of keys) { 158 | const pattern = typeof k === 'string' ? new RegExp(k) : k 159 | if (pattern.test(key)) { 160 | return pattern 161 | } 162 | } 163 | return undefined 164 | } 165 | 166 | /** 167 | * 查找对象某键的值,但是正则匹配 168 | * @param obj 查找的对象,对象的键作为正则式与 key 匹配 169 | * @param key 待匹配的字符串 170 | * @returns 171 | */ 172 | export const getValueByPattern = (obj: { [k: string]: T }, key: string): T | undefined => { 173 | for (const k of Object.keys(obj)) { 174 | const pattern = new RegExp(k) 175 | if (pattern.test(key)) { 176 | return obj[k] 177 | } 178 | } 179 | return undefined 180 | } 181 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/utils/path/uri.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将 uri 字符串解析为数组 3 | * @param uri 4 | * @returns 5 | */ 6 | export const uri2strArray = (uri: string) => { 7 | // todo: 转义 8 | if (uri.startsWith('#')) { 9 | // Decode URI fragment representation. 10 | uri = decodeURIComponent(uri.substring(1)) 11 | return uri.split('/').filter((v) => v) 12 | } else { 13 | throw new Error(`Could not find a definition for ${uri}.`) 14 | } 15 | } 16 | 17 | /** 18 | * 19 | * @param array 20 | * @returns 21 | */ 22 | export const strArray2uri = (array: string[]) => { 23 | // todo: 转义 24 | return '#/' + array.join('/') 25 | } 26 | -------------------------------------------------------------------------------- /src/JsonSchemaEditor/utils/schemaWithRef.ts: -------------------------------------------------------------------------------- 1 | import clone from 'lodash/clone' 2 | import { addRef, deepGet, deepCollect, jsonDataType, getValueByPattern } from '.' 3 | import { MergedSchema } from '../context/mergeSchema' 4 | import { JSONSchema } from '../type/Schema' 5 | import { uri2strArray } from './path/uri' 6 | 7 | /** 8 | * 找到 $ref 引用的 schemaMap。如果找不到返回一个空 Map 9 | * @param $ref json-pointer with # 10 | * @param rootSchema 11 | * @param deepSearch 深入搜索:深递归加入所有schema引用,否则只递归到浅一层 12 | * @returns 13 | */ 14 | export const getRefSchemaMap = ( 15 | $ref: string | undefined | string[], 16 | rootSchema = {}, 17 | deepSearch = false 18 | ): Map => { 19 | const schemaMap = new Map() 20 | const refQueue = $ref instanceof Array ? clone($ref) : $ref ? [$ref] : [] 21 | while (refQueue.length > 0) { 22 | const nowRef = addRef(refQueue.shift()!)! 23 | const current = deepGet(rootSchema, uri2strArray(nowRef)) 24 | if (current !== undefined) { 25 | schemaMap.set(nowRef, current) 26 | if (deepSearch) { 27 | const refs = deepCollect(current, '$ref') 28 | if (refs instanceof Array) { 29 | for (const ref of refs) { 30 | if (typeof ref === 'string' && !schemaMap.has(addRef(ref))) refQueue.push(ref) 31 | } 32 | } 33 | } else { 34 | if (current.hasOwnProperty('$ref') && !schemaMap.has(addRef(current.$ref))) { 35 | refQueue.push(current.$ref) 36 | } 37 | } 38 | } 39 | } 40 | return schemaMap 41 | } 42 | 43 | /** 44 | * 找到 schemaMap 中所有含有某键的 uri 引用 45 | * @param schemaMap 46 | * @param k 47 | * @param all 是否拿到所有 ref ,默认只拿第一个 48 | * @param add 返回的ref是否加入属性名称,默认加入 49 | * @returns 50 | */ 51 | // export const findKeyRefs = (schemaMap: Map, k: string, all = false, add = true) => { 52 | // const allRefs = [] 53 | // for (const [ref, schema] of schemaMap) { 54 | // if (schema instanceof Object && schema[k] !== undefined) { 55 | // if (all) { 56 | // allRefs.push(add ? addRef(ref, k)! : ref) 57 | // } else { 58 | // return add ? addRef(ref, k)! : ref 59 | // } 60 | // } 61 | // } 62 | // return all ? allRefs : undefined 63 | // } 64 | 65 | /** 66 | * 对 Array/Object 获取特定字段 schemaEntry。 67 | * 注意: 68 | * 1. 无论这其中有多少条 ref,一定保证给出的 schemaEntry 是从 `properties, patternProperties, additionalProperties, prefixItems, items` 这五个字段之一进入的。 69 | * 2. 只要给出的 ref 不是 undefined,一定能够找到对应的 schema 70 | * @param data 当前的 data 71 | * @param valueEntry 72 | * @param mergedValueSchema 73 | * @param field 74 | * @returns 对应子字段的 schemaEntry,结果为 false 代表对应 schema 为 false,在模式支持上当作 undefined 处理即可 75 | */ 76 | export const getFieldSchema = ( 77 | data: any, 78 | valueEntry: string | undefined, 79 | mergedValueSchema: MergedSchema | false, 80 | field: string 81 | ): string | false | undefined => { 82 | if (!mergedValueSchema) return undefined 83 | 84 | const { properties, patternProperties, additionalProperties, items, prefixItems } = mergedValueSchema 85 | const dataType = jsonDataType(data) 86 | switch (dataType) { 87 | case 'object': 88 | if (properties && properties[field]) return properties[field] 89 | 90 | if (patternProperties) { 91 | const ref = getValueByPattern(patternProperties, field) 92 | if (ref) return ref 93 | } 94 | 95 | if (additionalProperties !== undefined) return additionalProperties 96 | // 如果上面这些属性都没有但是 type = object,会使得这里到了下面的 case 而出错 97 | return undefined 98 | case 'array': 99 | /** 100 | * 注意:draft 2020-12 调整了items,删除了additionalItems属性。 101 | * 个人认为这么做是正确的,但是旧的没改,还需要兼容 102 | * 详情见:https://json-schema.org/draft/2020-12/release-notes.html 103 | */ 104 | const index = parseInt(field, 10) 105 | if (isNaN(index) || index < 0) { 106 | throw new Error( 107 | `获取字段模式错误:在数组中获取非法索引 ${field}\n辅助信息:${valueEntry} \n ${JSON.stringify( 108 | mergedValueSchema, 109 | null, 110 | 2 111 | )}` 112 | ) 113 | } 114 | if (prefixItems) { 115 | const { length, ref } = prefixItems 116 | if (index < length) { 117 | return addRef(ref, field) 118 | } else { 119 | return items 120 | } 121 | } else if (items) { 122 | return items 123 | } else { 124 | return undefined 125 | } 126 | default: 127 | throw new Error( 128 | `获取字段模式错误:在非对象中获取字段模式 ${field}\n辅助信息:${valueEntry} \n ${JSON.stringify( 129 | mergedValueSchema, 130 | null, 131 | 2 132 | )}` 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import $meta from './JsonSchemaEditor/$meta.json'; 2 | import JsonSchemaEditor from './JsonSchemaEditor'; 3 | 4 | export default JsonSchemaEditor; 5 | export const metaSchema = $meta; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "esModuleInterop": true, 8 | "types": ["jest"], 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true, 12 | "declaration": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["demos/**", "jest.config.ts", "__test__/**"] 16 | } 17 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | --------------------------------------------------------------------------------
{id}