├── .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 © 2020
Powered 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 © 2020
Powered 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 | 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 | 72 | 75 |
76 | ) : ( 77 | 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 | 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 |
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 | { 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 |