├── .babelrc ├── .browserslistrc ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── config ├── postcss.config.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── docs └── img │ ├── example-entity-referencing.gif │ ├── example-json-document.gif │ ├── example-json-schema.gif │ ├── example-user-entity.gif │ ├── example-zoom-in-out.gif │ ├── migcast-db-schema.png │ ├── movie-lens-db-schema.png │ └── schema-visualizer-social-banner-cut.png ├── package.json ├── src ├── css │ ├── custom-jointjs.css │ ├── custom.css │ ├── schema-diagram.css │ └── schema-editor.css ├── favicon.ico ├── index.html └── js │ ├── font-awesome-custom.js │ ├── index.js │ ├── jointjs-helper │ ├── cell-collapse-example.js │ ├── diagram-generator.js │ ├── jointjs-helper.js │ ├── schema-examples.js │ └── template-generator.js │ ├── json-editor │ ├── json-editor.js │ └── schema-validators.js │ ├── modal-template.html │ ├── schema-diagram │ ├── common │ │ ├── hierarchy-base-view.js │ │ ├── hierarchy-base.js │ │ └── html-element.js │ ├── diagram-root │ │ ├── diagram-root-view.js │ │ ├── diagram-root.html │ │ ├── diagram-root.js │ │ └── index.js │ ├── diagram-title │ │ ├── diagram-title.html │ │ ├── diagram-title.js │ │ └── index.js │ ├── object-row-header │ │ ├── object-row-header.html │ │ └── object-row-header.js │ ├── object-row │ │ ├── object-row.html │ │ └── object-row.js │ ├── simple-row │ │ ├── simple-row.html │ │ └── simple-row.js │ ├── supertype │ │ ├── supertype-view.js │ │ └── supertype.js │ └── utils │ │ ├── append-values.js │ │ ├── index.js │ │ ├── initialize-box.js │ │ ├── remove-box.js │ │ ├── render-box.js │ │ └── update-box.js │ ├── ui-script.js │ └── visual-schema-editor │ ├── html-to-json-schema │ └── index.js │ ├── index.js │ ├── object-row │ ├── index.js │ └── template.html │ └── simple-row │ ├── index.js │ └── template.html └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", 7 | "corejs": "3.0.0" 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-proposal-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production staging] 2 | >5% 3 | last 2 versions 4 | Firefox ESR 5 | not ie < 11 6 | 7 | [development] 8 | last 1 chrome version 9 | last 1 firefox version 10 | last 1 edge version 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 6, 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "semi": 2 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | build 6 | public 7 | 8 | # misc 9 | .DS_Store 10 | 11 | npm-debug.log 12 | yarn-error.log 13 | yarn.lock 14 | .yarnclean 15 | .vscode 16 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shamil Nabiyev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Schema Visualizer 6 | 7 | Schema Visualizer is an online data modeling tool for JSON and document based databases (document stores). It also can be used to visualize JSON data and JSON-Schema. 8 | 9 | ## Installation 10 | 11 | ```yarn install``` 12 | 13 | ## Development 14 | 15 | ```yarn run dev``` 16 | 17 | ## Production build 18 | 19 | ```yarn run build``` 20 | 21 | ## Used libraries 22 | 23 | * Bootstrap: https://github.com/twbs/bootstrap 24 | * JointJS: https://github.com/clientIO/joint 25 | * JSON Editor: https://github.com/josdejong/jsoneditor 26 | * JSON Schema Generator: https://github.com/mowgliLab/json-s-gen 27 | * Webpack: https://github.com/webpack/webpack 28 | 29 | ## Examples 30 | 31 | ### How to create a new entity type 32 | 33 | 34 | 35 | --- 36 | 37 | ### How to reference an entity 38 | 39 | 40 | 41 | --- 42 | 43 | ### How to zoom in/out 44 | 45 | 46 | 47 | --- 48 | 49 | ### How to import a JSON document 50 | 51 | 52 | 53 | --- 54 | 55 | ### How to import a JSON-Schema 56 | 57 | 58 | 59 | --- 60 | 61 | ### MovieLens Database Schema Diagrams 62 | 63 | 64 | 65 | --- 66 | 67 | ### MigCast Database Schema Diagrams 68 | 69 | 70 | 71 | --- 72 | -------------------------------------------------------------------------------- /config/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer') 4 | ] 5 | } -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | app: Path.resolve(__dirname, '../src/js/index.js') 9 | }, 10 | output: { 11 | path: Path.join(__dirname, '../public'), 12 | filename: 'js/[name].js' 13 | }, 14 | optimization: { 15 | splitChunks: { 16 | chunks: 'all', 17 | name: false 18 | } 19 | }, 20 | plugins: [ 21 | new CleanWebpackPlugin(), 22 | new CopyWebpackPlugin([ 23 | { from: Path.resolve(__dirname, '../public'), to: 'public' } 24 | ]), 25 | new HtmlWebpackPlugin({ 26 | template: Path.resolve(__dirname, '../src/', 'index.html'), 27 | favicon: Path.join(__dirname, '../src/', 'favicon.ico'), 28 | }), 29 | ], 30 | resolve: { 31 | modules: ['node_modules', 'src'], 32 | alias: { 33 | '~': Path.resolve(__dirname, '../src'), 34 | normalize_css: Path.join(__dirname, '../node_modules/normalize.css'), 35 | fontawesome_min_css: Path.join(__dirname, '../node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css'), 36 | fontawesome_solid_min_css: Path.join(__dirname, '../node_modules/@fortawesome/fontawesome-free/css/solid.min.css'), 37 | } 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.html$/, 43 | loader: "html-loader" 44 | }, 45 | { 46 | test: /\.mjs$/, 47 | include: /node_modules/, 48 | type: 'javascript/auto' 49 | }, 50 | { 51 | test: /\.(ico|jpg|jpeg|png|gif|webp)(\?.*)?$/, 52 | use: { 53 | loader: 'file-loader', 54 | options: { 55 | name: '[path][name].[ext]' 56 | } 57 | } 58 | }, 59 | { 60 | test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/, 61 | use: [ 62 | { 63 | loader: 'file-loader', 64 | options: { 65 | name: '[name].[ext]', 66 | outputPath: 'fonts/' 67 | } 68 | } 69 | ] 70 | } 71 | ] 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'development', 8 | devtool: 'cheap-eval-source-map', 9 | output: { 10 | chunkFilename: 'js/[name].chunk.js' 11 | }, 12 | devServer: { 13 | inline: true 14 | }, 15 | plugins: [ 16 | new Webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': JSON.stringify('development') 18 | }) 19 | ], 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.(js)$/, 24 | include: Path.resolve(__dirname, '../src'), 25 | enforce: 'pre', 26 | loader: 'eslint-loader', 27 | options: { 28 | emitWarning: true, 29 | } 30 | }, 31 | { 32 | test: /\.(js)$/, 33 | include: Path.resolve(__dirname, '../src'), 34 | loader: 'babel-loader' 35 | }, 36 | { 37 | test: /\.(css)$/, 38 | use: ['style-loader', 'css-loader?sourceMap=true', 'sass-loader'] 39 | }, 40 | { 41 | test: /\.(scss)$/, 42 | use: [{ 43 | loader: 'style-loader', // inject CSS to page 44 | }, { 45 | loader: 'css-loader', // translates CSS into CommonJS modules 46 | }, { 47 | loader: 'postcss-loader', // Run post css actions 48 | options: { 49 | plugins: function () { // post css plugins, can be exported to postcss.config.js 50 | return [ 51 | require('precss'), 52 | require('autoprefixer') 53 | ]; 54 | } 55 | } 56 | }, { 57 | loader: 'sass-loader' // compiles Sass to CSS 58 | }] 59 | } 60 | ] 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const common = require('./webpack.common.js'); 6 | 7 | module.exports = merge(common, { 8 | mode: 'production', 9 | devtool: 'source-map', 10 | stats: 'errors-only', 11 | bail: true, 12 | output: { 13 | filename: 'js/[name].[chunkhash:8].js', 14 | chunkFilename: 'js/[name].[chunkhash:8].chunk.js' 15 | }, 16 | plugins: [ 17 | new Webpack.DefinePlugin({ 18 | 'process.env.NODE_ENV': JSON.stringify('production') 19 | }), 20 | new Webpack.optimize.ModuleConcatenationPlugin(), 21 | new MiniCssExtractPlugin({ 22 | filename: 'bundle.css' 23 | }) 24 | ], 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js)$/, 29 | exclude: /node_modules/, 30 | use: 'babel-loader' 31 | }, 32 | { 33 | test: /\.s?css/i, 34 | use : [ 35 | MiniCssExtractPlugin.loader, 36 | 'css-loader', 37 | 'sass-loader' 38 | ] 39 | } 40 | ] 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /docs/img/example-entity-referencing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/example-entity-referencing.gif -------------------------------------------------------------------------------- /docs/img/example-json-document.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/example-json-document.gif -------------------------------------------------------------------------------- /docs/img/example-json-schema.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/example-json-schema.gif -------------------------------------------------------------------------------- /docs/img/example-user-entity.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/example-user-entity.gif -------------------------------------------------------------------------------- /docs/img/example-zoom-in-out.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/example-zoom-in-out.gif -------------------------------------------------------------------------------- /docs/img/migcast-db-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/migcast-db-schema.png -------------------------------------------------------------------------------- /docs/img/movie-lens-db-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/movie-lens-db-schema.png -------------------------------------------------------------------------------- /docs/img/schema-visualizer-social-banner-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/docs/img/schema-visualizer-social-banner-cut.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Schema Visualizer v2.0 for NoSQL DBMS", 3 | "name": "schema-visualizer-v2", 4 | "version": "2.0.0", 5 | "scripts": { 6 | "build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js --colors", 7 | "dev": "webpack-dev-server --config config/webpack.dev.js" 8 | }, 9 | "license": "MIT", 10 | "private": true, 11 | "keywords": [ 12 | "JSON", 13 | "JSON-Schema", 14 | "NoSQL", 15 | "database Modeling", 16 | "Schema Visualisation" 17 | ], 18 | "devDependencies": { 19 | "@babel/core": "^7.4.0", 20 | "@babel/plugin-proposal-class-properties": "^7.4.0", 21 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 22 | "@babel/preset-env": "^7.6.2", 23 | "@fortawesome/fontawesome-svg-core": "^1.2.25", 24 | "@fortawesome/free-brands-svg-icons": "^5.11.2", 25 | "@fortawesome/free-regular-svg-icons": "^5.11.2", 26 | "@fortawesome/free-solid-svg-icons": "^5.11.2", 27 | "babel-loader": "^8.0.6", 28 | "clean-webpack-plugin": "^2.0.2", 29 | "copy-webpack-plugin": "^5.0.3", 30 | "cross-env": "^6.0.0", 31 | "css-loader": "^3.0.0", 32 | "eslint": "^6.0.0", 33 | "eslint-loader": "^3.0.1", 34 | "file-loader": "^4.0.0", 35 | "html-loader": "^0.5.5", 36 | "html-webpack-plugin": "^4.0.0-beta.5", 37 | "mini-css-extract-plugin": "^0.8.0", 38 | "node-sass": "^4.12.0", 39 | "postcss-loader": "^3.0.0", 40 | "precss": "^4.0.0", 41 | "sass-loader": "^8.0.0", 42 | "style-loader": "^1.0.0", 43 | "webpack": "^4.29.6", 44 | "webpack-cli": "^3.3.9", 45 | "webpack-dev-server": "^3.8.1", 46 | "webpack-merge": "^4.2.1" 47 | }, 48 | "dependencies": { 49 | "@fortawesome/fontawesome-free": "^5.11.2", 50 | "@jdw/jst": "^2.0.0-beta.15", 51 | "backbone": "^1.4.0", 52 | "bootstrap": "^4.3.1", 53 | "core-js": "^3.2.1", 54 | "generate-schema": "^2.6.0", 55 | "himalaya": "^1.1.0", 56 | "jointjs": "^3.0.4", 57 | "jquery": "^3.4.1", 58 | "json-s-generator": "^0.0.4", 59 | "json-schema-ref-parser": "^7.1.1", 60 | "json-schema-traverse": "^0.4.1", 61 | "jsoneditor": "^7.2.0", 62 | "lodash": "^4.17.15", 63 | "uuid": "^3.3.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/css/custom-jointjs.css: -------------------------------------------------------------------------------- 1 | .joint-paper { 2 | border: 1px solid lightgray; 3 | display: inline-block; 4 | overflow: hidden; 5 | } 6 | 7 | #paper-multiple-papers-small { 8 | width: 300px; 9 | } 10 | 11 | #paper-html-elements { 12 | position: relative; 13 | border: 1px solid #cccccc; 14 | display: inline-block; 15 | background: transparent; 16 | overflow: hidden; 17 | margin-bottom: 1rem; 18 | } 19 | 20 | #paper-html-elements svg { 21 | background: transparent; 22 | } 23 | 24 | #paper-html-elements svg .link { 25 | z-index: 2; 26 | } 27 | 28 | .html-element { 29 | position: absolute; 30 | pointer-events: none; 31 | -webkit-user-select: none; 32 | z-index: 2; 33 | } 34 | 35 | 36 | circle.port-body, 37 | circle.joint-port-body { 38 | opacity: 0; 39 | fill: yellow; 40 | stroke: #424242; 41 | } 42 | 43 | 44 | circle.port-body:hover, 45 | circle.joint-port-body:hover { 46 | opacity: 1; 47 | } 48 | 49 | 50 | /* port styling */ 51 | .available-magnet { 52 | fill: yellow; 53 | } 54 | 55 | /* element styling */ 56 | .available-cell rect { 57 | stroke-dasharray: 5, 2; 58 | } 59 | 60 | #paper-holder { 61 | height: 65vh !important; 62 | flex-grow: 1; 63 | } -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | .vertical-divider { 2 | display: inline; 3 | border-right: 1px solid #aaaaaa; 4 | } 5 | 6 | .modal-body { 7 | height: 70vh; 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | .jsoneditor-wrapper { 12 | flex-grow: 1; 13 | /* height: 40vh; */ 14 | } 15 | 16 | .jsoneditor-menu { 17 | background-color: #0099ee; 18 | border-bottom: 1px solid #0068a2; 19 | } 20 | 21 | .jsoneditor { 22 | border: thin solid #0099ee; 23 | } 24 | 25 | .joint-tool { 26 | opacity: 0.01; 27 | transition: opacity 0.5s; 28 | } 29 | 30 | .joint-layers:hover .joint-tool { 31 | transition: opacity 0.5s; 32 | opacity: 1; 33 | } 34 | 35 | #schema-editor-modal .modal-content { 36 | /*height: 80vh;*/ 37 | } 38 | 39 | #entity-type-name-form { 40 | border: 1px solid #cccccc; 41 | } -------------------------------------------------------------------------------- /src/css/schema-diagram.css: -------------------------------------------------------------------------------- 1 | .flex-container { 2 | height: 100%; 3 | display: flex; 4 | flex-wrap: nowrap; 5 | justify-content: space-between; 6 | background-color: #ffffff; 7 | border-bottom: 1px solid #7d7d7d; 8 | border-left: 1px solid #7d7d7d; 9 | border-right: 1px solid #7d7d7d; 10 | } 11 | 12 | .in_port { 13 | padding: 0.5em 0.5em; 14 | } 15 | 16 | .out_port { 17 | display: flex; 18 | flex-wrap: nowrap; 19 | } 20 | 21 | .field_constraints { 22 | font-style: italic; 23 | color: #aaa; 24 | padding: 0.5em 0.5em; 25 | } 26 | 27 | .field_date_type { 28 | min-width: 5em; 29 | text-align: right; 30 | padding: 0.5em 0.5em; 31 | } 32 | 33 | .header { 34 | height: 100%; 35 | width: 100%; 36 | padding: 0.2em 0.5em; 37 | text-align: center; 38 | background-color: #0099ee; 39 | color: #fff; 40 | border: 1px solid #00598a; 41 | } 42 | 43 | .row-placeholder { 44 | padding: 0 0.25rem; 45 | color: #cccccc; 46 | } 47 | 48 | .row-expander { 49 | cursor: pointer; 50 | } 51 | 52 | 53 | .fa-caret-right { 54 | -moz-transition: all 200ms linear; 55 | -webkit-transition: all 200ms linear; 56 | transition: all 200ms linear; 57 | } 58 | 59 | .fa-caret-right.down { 60 | -moz-transform: rotate(90deg); 61 | -webkit-transform: rotate(90deg); 62 | transform: rotate(90deg); 63 | } 64 | 65 | .html-element .flex-container { 66 | /* Enable interacting with inputs only. */ 67 | pointer-events: auto; 68 | } 69 | -------------------------------------------------------------------------------- /src/css/schema-editor.css: -------------------------------------------------------------------------------- 1 | #editor, #test { 2 | margin: 1em auto; 3 | width: 800px; 4 | padding: 1em; 5 | } 6 | 7 | #schema-editor { 8 | border: 1px solid #cccccc; 9 | overflow-y: auto; 10 | flex-grow: 1; 11 | } 12 | 13 | .new-prop-block, 14 | .new-field-elements { 15 | margin-top: -1px; 16 | padding: 0.5em 0.5em 0.5em 1em; 17 | } 18 | 19 | .properties { 20 | padding-left: 1em; 21 | } 22 | 23 | .object-row { 24 | margin-bottom: -1px; 25 | border-left: 1px solid #cccccc; 26 | border-bottom: 1px solid #cccccc; 27 | } 28 | 29 | #schema-editor > .object-row { 30 | border-left: none; 31 | } 32 | 33 | .simple-row { 34 | padding: 0.3em 0 0.3em 1em; 35 | border-left: 1px solid #cccccc; 36 | border-bottom: 1px solid #cccccc; 37 | display: flex; 38 | justify-content: space-between; 39 | } 40 | 41 | .object-row > .simple-row { 42 | border-left: none; 43 | border-right: none; 44 | margin-bottom: -1px; 45 | } 46 | 47 | .empty-placeholder { 48 | font-style: italic; 49 | } 50 | 51 | .new-field-elements { 52 | border-top: 1px solid #cccccc; 53 | } 54 | 55 | .add-btn, 56 | .cancel-btn { 57 | cursor: pointer; 58 | } 59 | 60 | .add-btn:disabled { 61 | cursor: not-allowed; 62 | } 63 | 64 | .remove-btn:disabled { 65 | cursor: not-allowed; 66 | } 67 | 68 | .field-type { 69 | font-style: italic; 70 | } 71 | 72 | 73 | .simple-row .remove-btn-block { 74 | width: 25%; 75 | } 76 | 77 | .remove-btn-block { 78 | display: flex; 79 | justify-content: flex-end; 80 | } 81 | 82 | .property-info { 83 | width: 75%; 84 | display: flex; 85 | justify-content: space-between; 86 | } 87 | 88 | #parser-btn { 89 | float: right; 90 | } 91 | 92 | #form-schema-editor-title { 93 | border: 1px solid #cccccc; 94 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamilnabiyev/schema-visualizer/7e14552ec83d8c9978e1fe7feda8ac2b9a7de54e/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Schema Visualizer 11 | 102 | 103 | 104 | 105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 | 119 | 435 | 436 | 437 | 438 | 439 | 440 | -------------------------------------------------------------------------------- /src/js/font-awesome-custom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Font Awesome 5 3 | * @link https://fontawesome.com/how-to-use/with-the-api/ 4 | */ 5 | 6 | import { library, dom } from '@fortawesome/fontawesome-svg-core'; 7 | 8 | // Solid 9 | // https://fontawesome.com/icons?d=gallery&s=solid&m=free 10 | import { 11 | faBars, 12 | faBorderStyle, 13 | faCheck, 14 | faFlag, 15 | faInfoCircle, 16 | faExclamationTriangle, 17 | faTrash, 18 | faSearchPlus, 19 | faSearchMinus, 20 | faFolder, 21 | faGripHorizontal, 22 | faExpand, 23 | faArrowsAltH, 24 | faPrint, 25 | faCaretRight, 26 | faFileCode, 27 | faBan, 28 | faPencilAlt, 29 | faChevronDown, 30 | faChevronRight, 31 | faPlus, 32 | faCheckCircle, 33 | faTrashAlt, 34 | faTimesCircle, 35 | faFileImport, 36 | faEdit, 37 | faStream, 38 | faSave, 39 | faFolderOpen 40 | } from '@fortawesome/free-solid-svg-icons'; 41 | 42 | import { 43 | faFile, 44 | faFilePdf, 45 | faImage, 46 | faCaretSquareRight, 47 | } from '@fortawesome/free-regular-svg-icons'; 48 | 49 | 50 | library.add(faBars, faBorderStyle, faCheck, faFlag, faInfoCircle, faExclamationTriangle, 51 | faTrash, faSearchPlus, faSearchMinus, faFolder, faGripHorizontal, faExpand, 52 | faArrowsAltH, faFile, faSave, faPrint, faFilePdf, faImage, faCaretRight, 53 | faCaretSquareRight, faFileCode, faCheckCircle, faBan, faPencilAlt, faTrashAlt, faChevronDown, 54 | faChevronRight, faPlus, faTimesCircle, faFileImport, faEdit, faStream, faFolderOpen); 55 | 56 | 57 | // automatically find any tags in the page and replace those with elements 58 | // https://fontawesome.com/how-to-use/with-the-api/methods/dom-i2svg 59 | dom.i2svg(); 60 | 61 | // or 62 | 63 | // if content is dynamic 64 | // watch the DOM automatic for any additional icons being added or modified 65 | // https://fontawesome.com/how-to-use/with-the-api/methods/dom-watch 66 | // dom.watch() 67 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | /* Import the core libs */ 2 | import $ from 'jquery'; 3 | import 'bootstrap/dist/js/bootstrap.bundle'; 4 | import './ui-script.js'; 5 | import './font-awesome-custom'; 6 | import './json-editor/json-editor'; 7 | import './visual-schema-editor'; 8 | /* Import the custom classes */ 9 | import { 10 | getPaper, 11 | addMigCastDbDiagrams, 12 | addSpeciesDbDiagrams, 13 | addMovieLensDbSchemata, 14 | serializeDiagrams, 15 | deserializeDiagrams, 16 | } from "./jointjs-helper/diagram-generator"; 17 | import 'bootstrap/dist/css/bootstrap.min.css'; 18 | import 'jointjs/dist/joint.min.css'; 19 | import 'jsoneditor/dist/jsoneditor.min.css'; 20 | import '@fortawesome/fontawesome-free/css/fontawesome.min.css'; 21 | import '@fortawesome/fontawesome-free/css/solid.min.css'; 22 | import '../css/custom.css'; 23 | import '../css/custom-jointjs.css'; 24 | import '../css/schema-diagram.css'; 25 | import '../css/schema-editor.css'; 26 | 27 | $(window).on("load", () => { 28 | $('#loading-icon').remove(); 29 | $('#wrapper').css("visibility", "initial"); 30 | }); 31 | 32 | /** 33 | * An example showing how to collapse/expand elements in jointjs 34 | * 35 | * URL: https://jsfiddle.net/vsd21my5/5/ 36 | */ 37 | const paper = getPaper(); 38 | 39 | $("#zoom-in-btn").on('click', () => { 40 | if(!paper) return; 41 | 42 | const scale = paper.scale(); 43 | const newScaleX = scale.sx + 0.05; 44 | const newScaleY = scale.sy + 0.05; 45 | if (newScaleX >= 0.2 && newScaleX <= 2) paper.scale(newScaleX, newScaleY); 46 | }); 47 | 48 | $("#zoom-out-btn").on('click', () => { 49 | if(!paper) return; 50 | 51 | const scale = paper.scale(); 52 | const newScaleX = scale.sx - 0.05; 53 | const newScaleY = scale.sy - 0.05; 54 | if (newScaleX >= 0.2 && newScaleX <= 2) paper.scale(newScaleX, newScaleY); 55 | }); 56 | 57 | $("#zoom-reset-btn").on('click', () => { 58 | if(!paper) return; 59 | 60 | paper.scale(0.7, 0.7); 61 | }); 62 | 63 | $('#migcast-db-btn').on('click', addMigCastDbDiagrams); 64 | $('#species-db-btn').on('click', addSpeciesDbDiagrams); 65 | $('#movielens-db-btn').on('click', addMovieLensDbSchemata); 66 | $('#serialization-btn').on('click', serializeDiagrams); 67 | $('#deserialization-btn').on('click', deserializeDiagrams); -------------------------------------------------------------------------------- /src/js/jointjs-helper/cell-collapse-example.js: -------------------------------------------------------------------------------- 1 | var graph = new joint.dia.Graph(); 2 | var paper = new joint.dia.Paper({ 3 | el: document.getElementById('paper'), 4 | model: graph, 5 | defaultConnectionPoint: { 6 | name: 'boundary' 7 | }, 8 | defaultAnchor: { 9 | name: 'center' 10 | } 11 | }); 12 | 13 | var Group = joint.dia.Element.define('example.Group', { 14 | size: { 15 | width: 100, 16 | height: 40 17 | }, 18 | attrs: { 19 | body: { 20 | refWidth: '100%', 21 | refHeight: '100%', 22 | fill: 'white', 23 | stroke: 'black', 24 | strokeWidth: 2 25 | }, 26 | tool: { 27 | r: 10, 28 | cx: 20, 29 | cy: 20, 30 | fill: 'white', 31 | stroke: 'blue', 32 | strokeWidth: 2, 33 | cursor: 'pointer', 34 | event: 'element:collapse' 35 | } 36 | } 37 | }, { 38 | markup: [{ 39 | tagName: 'rect', 40 | selector: 'body' 41 | }, { 42 | tagName: 'circle', 43 | selector: 'tool' 44 | }] 45 | }); 46 | 47 | // Setup 48 | 49 | var e1 = new Group(); 50 | e1.position(50, 50); 51 | var a1 = new joint.shapes.standard.Rectangle(); 52 | var a2 = new joint.shapes.standard.Rectangle(); 53 | a1.size(50, 50); 54 | a2.size(70, 30); 55 | var l1 = new joint.shapes.standard.Link(); 56 | l1.source(a1).target(a2); 57 | graph.addCells([e1, a1, a2, l1]); 58 | e1.embed(a1); 59 | e1.embed(a2); 60 | a1.position(20, 10, { 61 | parentRelative: true 62 | }); 63 | a2.position(90, 30, { 64 | parentRelative: true 65 | }); 66 | e1.fitEmbeds({ 67 | padding: { 68 | top: 40, 69 | left: 10, 70 | right: 10, 71 | bottom: 10 72 | } 73 | }); 74 | 75 | var e2 = new Group(); 76 | e2.position(300, 300); 77 | var a3 = new joint.shapes.standard.Rectangle(); 78 | var a4 = new joint.shapes.standard.Rectangle(); 79 | a3.size(50, 50); 80 | a4.size(70, 30); 81 | var l2 = new joint.shapes.standard.Link(); 82 | l2.source(a3).target(a4); 83 | graph.addCells([e2, a3, a4, l2]); 84 | e2.embed(a3); 85 | e2.embed(a4); 86 | a3.position(20, 10, { 87 | parentRelative: true 88 | }); 89 | a4.position(90, -20, { 90 | parentRelative: true 91 | }); 92 | e2.fitEmbeds({ 93 | padding: { 94 | top: 40, 95 | left: 10, 96 | right: 10, 97 | bottom: 10 98 | } 99 | }); 100 | toggleCollapse(e2); 101 | 102 | var l = new joint.shapes.standard.Link(); 103 | l.source(e1).target(e2); 104 | l.addTo(graph); 105 | 106 | // Collapse / Expand 107 | function toggleCollapse(group) { 108 | var graph = group.graph; 109 | if (!graph) return; 110 | var embeds; 111 | if (group.get('collapsed')) { 112 | group.transition('size/height', 150, { 113 | delay: 100, 114 | duration: 300, 115 | }); 116 | group.transition('size/width', 160, { 117 | delay: 100, 118 | duration: 300, 119 | }); 120 | 121 | group.on('transition:end', function (element, pathToAttribute) { 122 | if (group.get('collapsed')) return; 123 | // EXPAND 124 | var subgraph = group.get('subgraph'); 125 | // deserialize subgraph 126 | var tmpGraph = new joint.dia.Graph(); 127 | tmpGraph.addCells(subgraph); 128 | embeds = tmpGraph.getCells(); 129 | tmpGraph.removeCells(embeds); 130 | // set relative position to parent 131 | embeds.forEach(function (embed) { 132 | if (embed.isLink()) return; 133 | var diff = embed.position().offset(group.position()); 134 | embed.position(diff.x, diff.y); 135 | }); 136 | graph.addCells(embeds); 137 | embeds.forEach(function (embed) { 138 | if (embed.isElement()) { 139 | group.embed(embed); 140 | } else { 141 | embed.reparent(); 142 | } 143 | }); 144 | 145 | group.attr('tool/stroke', 'blue'); 146 | /* 147 | group.fitEmbeds({ 148 | padding: { 149 | top: 40, 150 | left: 10, 151 | right: 10, 152 | bottom: 10 153 | } 154 | }); 155 | */ 156 | }); 157 | group.set('collapsed', false); 158 | 159 | 160 | } else { 161 | 162 | // COLLAPSE 163 | embeds = graph.getSubgraph(group.getEmbeddedCells()); 164 | embeds.sort(function (a) { 165 | return a.isLink() ? 1 : -1; 166 | }); 167 | graph.removeCells(embeds); 168 | // get relative position to parent 169 | embeds.forEach(function (embed) { 170 | if (embed.isLink()) return; 171 | var diff = embed.position().difference(group.position()); 172 | embed.position(diff.x, diff.y); 173 | }); 174 | // serialize subgraph 175 | group.set('subgraph', embeds.map(function (embed) { 176 | return embed.toJSON(); 177 | })); 178 | // group.resize(100, 100); 179 | group.set('collapsed', true); 180 | // group.size(100, 40); 181 | 182 | group.set('collapsed', true); 183 | 184 | group.transition('size/height', 40, { 185 | delay: 100, 186 | duration: 300, 187 | }); 188 | group.transition('size/width', 100, { 189 | delay: 100, 190 | duration: 300, 191 | }); 192 | 193 | group.attr('tool/stroke', 'red'); 194 | } 195 | } 196 | 197 | // event handler for task group button 198 | paper.on('element:collapse', function (elementView, evt) { 199 | evt.stopPropagation(); 200 | toggleCollapse(elementView.model); 201 | }); 202 | -------------------------------------------------------------------------------- /src/js/jointjs-helper/diagram-generator.js: -------------------------------------------------------------------------------- 1 | import _isArray from 'lodash/isArray'; 2 | import _isEqual from 'lodash/isEqual'; 3 | import _isPlainObject from 'lodash/isPlainObject'; 4 | import _includes from 'lodash/includes'; 5 | import _isNil from 'lodash/isNil'; 6 | import _has from 'lodash/has'; 7 | import _forEach from 'lodash/forEach'; 8 | import _cloneDeep from 'lodash/cloneDeep'; 9 | import $ from "jquery"; 10 | import {dia, shapes} from "jointjs"; 11 | import { 12 | createTitleRow, createSimpleRow, createObjectRow, createPaper, createRect 13 | } from './jointjs-helper'; 14 | import DiagramRoot from "../schema-diagram/diagram-root/"; 15 | import {migCastDbSchemata, movieLensDbSchemata, speciesDbSchemata} from "./schema-examples"; 16 | import Supertype from "../schema-diagram/supertype/supertype"; 17 | 18 | let GRAPH = initGraph(); 19 | let PAPER = initPaper(); 20 | 21 | function initGraph() { 22 | return new dia.Graph({}, {cellNamespace: shapes}); 23 | } 24 | 25 | function getGraph() { 26 | if (_isNil(GRAPH)) GRAPH = initGraph(); 27 | return GRAPH; 28 | } 29 | 30 | function initPaper() { 31 | return createPaper($('#paper-html-elements'), getGraph()); 32 | } 33 | 34 | export const getPaper = function () { 35 | if (_isNil(PAPER)) PAPER = initPaper(); 36 | return PAPER; 37 | }; 38 | 39 | const FIFTY = 50; 40 | const TYPE = "type"; 41 | const PROPERTIES = 'properties'; 42 | const WIDTH = 400; 43 | const HEIGHT = 35; 44 | let X_START = 50; 45 | let Y_START = 50; 46 | const REQ_FRAG = "req"; 47 | const OPT_FLAG = " "; 48 | let requiredProps; 49 | 50 | /** 51 | * 52 | * @param {String} key The json schema property name 53 | * @param {Object} value The property itself containing key value pairs such as data type, properties etc. 54 | * @param {{value: number}} rowLevel 55 | */ 56 | const simpleRow = (value, key, rowLevel) => createSimpleRow({ 57 | field_name: key, 58 | field_constraints: (_includes(requiredProps, key)) ? REQ_FRAG : OPT_FLAG, 59 | field_date_type: value[TYPE], 60 | width: WIDTH, height: HEIGHT, 61 | x: X_START, 62 | rowLevel: rowLevel 63 | }); 64 | 65 | const objectRow = (value, key, rowLevel) => createObjectRow({ 66 | field_name: key, 67 | field_constraints: (_includes(requiredProps, key)) ? REQ_FRAG : OPT_FLAG, 68 | field_date_type: value[TYPE], 69 | width: WIDTH, height: HEIGHT, 70 | x: X_START, 71 | rowLevel: rowLevel 72 | }); 73 | 74 | const SIMPLE_TYPES = ["boolean", "integer", "null", "number", "string"]; 75 | const OBJECT_TYPE = "object"; 76 | const ARRAY_TYPE = "array"; 77 | const ITEMS = "items"; 78 | const ONE_OF = "oneOf"; 79 | 80 | function generateRows(properties, doc, rowLevel) { 81 | _forEach(properties, (property, key) => { 82 | if (_includes(SIMPLE_TYPES, property.type)) { 83 | addSimpleRow(doc, key, property, rowLevel); 84 | } else if (_isEqual(property.type, OBJECT_TYPE)) { 85 | addDocumentRow(doc, key, property, rowLevel); 86 | } else if (_isEqual(property.type, ARRAY_TYPE)) { 87 | addArrayRow(doc, key, property, rowLevel); 88 | } 89 | }); 90 | } 91 | 92 | function addSimpleRow(doc, key, property, rowLevel) { 93 | const newSimpleRow = simpleRow(property, key, rowLevel.value); 94 | doc.addSimpleRow(newSimpleRow); 95 | } 96 | 97 | function addDocumentRow(doc, key, property, rowLevel) { 98 | const subDoc = objectRow(property, key, rowLevel.value); 99 | doc.addObjectRow(subDoc); 100 | 101 | if (!_has(property, PROPERTIES)) return; 102 | rowLevel.value += 1; 103 | generateRows(property[PROPERTIES], subDoc, rowLevel); 104 | rowLevel.value -= 1; 105 | } 106 | 107 | function addArrayRow(doc, key, property, rowLevel) { 108 | const arrayRow = objectRow(property, key, rowLevel.value); 109 | doc.addObjectRow(arrayRow); 110 | 111 | if (!_has(property, ITEMS)) return; 112 | if (_has(property, [ITEMS, ONE_OF])) { 113 | property[ITEMS] = [...new Set(property[ITEMS][ONE_OF].map(JSON.stringify))].map(JSON.parse); 114 | } 115 | 116 | rowLevel.value += 1; 117 | if (_isPlainObject(property[ITEMS]) && !_isArray(property[ITEMS])) { 118 | addArrayItems(arrayRow, property[ITEMS], '[0]', rowLevel); 119 | } else if (_isArray(property[ITEMS])) { 120 | _forEach(property[ITEMS], (elem, elemIndex) => { 121 | addArrayItems(arrayRow, elem, `[${elemIndex}]`, rowLevel); 122 | }); 123 | } 124 | rowLevel.value -= 1; 125 | } 126 | 127 | function addArrayItems(arrayRow, items, key, rowLevel) { 128 | if (_has(items, TYPE) && _isEqual(items[TYPE], OBJECT_TYPE)) { 129 | const itemsRow = objectRow(items, key, rowLevel.value); 130 | arrayRow.addObjectRow(itemsRow); 131 | 132 | rowLevel.value += 1; 133 | generateRows(items[PROPERTIES], itemsRow, rowLevel); 134 | rowLevel.value -= 1; 135 | } else if (_has(items, TYPE) && _includes(SIMPLE_TYPES, items[TYPE])) { 136 | addSimpleRow(arrayRow, key, items, rowLevel); 137 | } 138 | } 139 | 140 | function createCellsFrom(diagramRoot, schema) { 141 | requiredProps = schema['required'] || []; 142 | const rowLevel = {value: 0}; 143 | generateRows(schema.properties, diagramRoot, rowLevel); 144 | 145 | diagramRoot.fitEmbeds(); 146 | 147 | let diagramRootHeight = diagramRoot.prop('size/height'); 148 | 149 | _forEach(diagramRoot.getSimpleRowList(), (simpleRow, index) => { 150 | GRAPH.addCell(simpleRow); 151 | diagramRoot.embed(simpleRow); 152 | simpleRow.position(0, diagramRootHeight + (index * HEIGHT), {parentRelative: true}); 153 | }); 154 | 155 | diagramRoot.fitEmbeds(); 156 | 157 | diagramRootHeight = diagramRoot.prop('size/height'); 158 | 159 | _forEach(diagramRoot.getObjectRowList(), (objectRow, index) => { 160 | const header = objectRow.getHeader(); 161 | GRAPH.addCell(header); 162 | objectRow.embed(header); 163 | 164 | GRAPH.addCell(objectRow); 165 | diagramRoot.embed(objectRow); 166 | objectRow.position(0, diagramRootHeight + (index * HEIGHT), {parentRelative: true}); 167 | 168 | header.position(0, 0, {parentRelative: true}); 169 | }); 170 | 171 | diagramRoot.fitEmbeds(); 172 | diagramRoot.toFront(); 173 | } 174 | 175 | export const createDiagramRoot = function (schema) { 176 | const SCHEMA = _cloneDeep(schema); 177 | 178 | const titleText = SCHEMA.title || "Entity_Type_" + Math.floor(X_START / FIFTY); 179 | if (_isNil(SCHEMA.title)) SCHEMA.title = titleText; 180 | 181 | const diagramRoot = new DiagramRoot.Element({ 182 | attrs: { 183 | text: {text: titleText}, 184 | }, 185 | position: {x: X_START, y: Y_START} 186 | }); 187 | 188 | X_START = X_START + FIFTY; 189 | Y_START = Y_START + FIFTY; 190 | 191 | GRAPH.addCell(diagramRoot); 192 | 193 | const diagramTitle = createTitleRow({ 194 | title: titleText, 195 | width: WIDTH, 196 | height: HEIGHT 197 | }); 198 | GRAPH.addCell(diagramTitle); 199 | diagramRoot.embed(diagramTitle); 200 | diagramTitle.position(0, 0, {parentRelative: true}); 201 | diagramRoot.setDiagramTitle(diagramTitle); 202 | diagramRoot.setSchema(SCHEMA); 203 | 204 | createCellsFrom(diagramRoot, SCHEMA); 205 | }; 206 | 207 | export const updateDiagramRoot = function (diagramRoot) { 208 | const deepEmbeds = diagramRoot.getEmbeddedCells({deep: true}); 209 | GRAPH.removeCells(deepEmbeds); 210 | diagramRoot.removeChildCells(); 211 | 212 | const diagramRootSchema = diagramRoot.getSchema(); 213 | 214 | const diagramTitle = createTitleRow({ 215 | title: diagramRootSchema['title'], 216 | width: WIDTH, 217 | height: HEIGHT 218 | }); 219 | 220 | GRAPH.addCell(diagramTitle); 221 | diagramRoot.embed(diagramTitle); 222 | diagramTitle.position(0, 0, {parentRelative: true}); 223 | diagramRoot.setDiagramTitle(diagramTitle); 224 | 225 | createCellsFrom(diagramRoot, diagramRootSchema); 226 | }; 227 | 228 | export const addRect = function () { 229 | const rect = createRect(); 230 | rect.resize(200, 200); 231 | 232 | const child = createRect(); 233 | rect.embed(child); 234 | 235 | GRAPH.addCells([rect, child]); 236 | // rect.toFront(); 237 | }; 238 | 239 | export const addMigCastDbDiagrams = function () { 240 | createDiagramRoot(migCastDbSchemata.simulations); 241 | createDiagramRoot(migCastDbSchemata.results); 242 | createDiagramRoot(migCastDbSchemata.stats); 243 | }; 244 | 245 | export const addSpeciesDbDiagrams = function () { 246 | createDiagramRoot(speciesDbSchemata.species); 247 | createDiagramRoot(speciesDbSchemata.protocols); 248 | }; 249 | 250 | export const addMovieLensDbSchemata = function () { 251 | createDiagramRoot(movieLensDbSchemata.users); 252 | createDiagramRoot(movieLensDbSchemata.links); 253 | createDiagramRoot(movieLensDbSchemata.movies); 254 | createDiagramRoot(movieLensDbSchemata.ratings); 255 | createDiagramRoot(movieLensDbSchemata.tags); 256 | }; 257 | 258 | let SERIALIZED_DATA = null; 259 | 260 | export const serializeDiagrams = function () { 261 | SERIALIZED_DATA = GRAPH.toJSON(); 262 | console.log(SERIALIZED_DATA); 263 | }; 264 | 265 | export const deserializeDiagrams = function () { 266 | if (_isNil(SERIALIZED_DATA)) { 267 | alert("No serialized data found"); 268 | return; 269 | } 270 | GRAPH.fromJSON(SERIALIZED_DATA); 271 | }; 272 | 273 | /** 274 | * TODO: to be removed 275 | * @deprecated 276 | */ 277 | export const createSupertype = function () { 278 | const person = new Supertype.Element({ 279 | position: {x: 200, y: 200} 280 | }); 281 | GRAPH.addCell(person); 282 | 283 | const diagramTitle = createTitleRow({ 284 | title: "Student", 285 | width: WIDTH, 286 | height: HEIGHT 287 | }); 288 | GRAPH.addCell(diagramTitle); 289 | 290 | const student = new DiagramRoot.Element({ 291 | attrs: {text: {text: "Student"}}, 292 | }); 293 | GRAPH.addCell(student); 294 | 295 | 296 | student.position(50, 50, {parentRelative: true}); 297 | // student.setDiagramTitle(diagramTitle); 298 | 299 | student.embed(diagramTitle); 300 | diagramTitle.position(0, 0, {parentRelative: true}); 301 | 302 | const firstName = simpleRow({"type": "integer"}, "first_name", {value: 0}); 303 | GRAPH.addCell(firstName); 304 | 305 | student.embed(firstName); 306 | firstName.position(0, 35, {parentRelative: true}); 307 | 308 | student.fitEmbeds(); 309 | student.toFront(); 310 | 311 | person.embed(student); 312 | // person.position(100, 100); 313 | 314 | // student.position(50, 50, {parentRelative: true}); 315 | 316 | person.fitEmbeds({ 317 | padding: { 318 | top: 50, 319 | left: 50, 320 | right: 50, 321 | bottom: 50 322 | } 323 | }); 324 | // person.toFront(); 325 | }; -------------------------------------------------------------------------------- /src/js/jointjs-helper/jointjs-helper.js: -------------------------------------------------------------------------------- 1 | import {dia, shapes, linkTools, elementTools} from 'jointjs'; 2 | import isUndefined from 'lodash/isUndefined'; 3 | import isNull from 'lodash/isNull'; 4 | import isFunction from 'lodash/isFunction'; 5 | import $ from 'jquery'; 6 | import SimpleRow from '../schema-diagram/simple-row/simple-row'; 7 | import DiagramTitle from '../schema-diagram/diagram-title/diagram-title'; 8 | import ObjectRow from "../schema-diagram/object-row/object-row"; 9 | import ObjectRowHeader from "../schema-diagram/object-row-header/object-row-header"; 10 | import DiagramRoot from "../schema-diagram/diagram-root/"; 11 | import {openSchemaUpdateModal} from "../json-editor/json-editor"; 12 | 13 | const cardinalityUpdateModal = $('#update-cardinality-modal'); 14 | const cardinalityUpdateButton = $('#update-cardinality-btn'); 15 | const cardinalitySelection = $('#cardinality-selection'); 16 | 17 | const getPosition = (options) => { 18 | return {x: options.x, y: options.y}; 19 | }; 20 | 21 | const getSize = (options) => { 22 | return {width: options.width, height: options.height}; 23 | }; 24 | 25 | export const PORT_OPTIONS = { 26 | groups: { 27 | 'in': { 28 | label: { 29 | position: { 30 | name: 'right', 31 | } 32 | } 33 | }, 34 | 'out': { 35 | label: { 36 | position: { 37 | name: 'left', 38 | } 39 | }, 40 | } 41 | } 42 | }; 43 | 44 | /** 45 | * 46 | * @returns {dia.Link} 47 | */ 48 | const createDefaultLink = function createDefaultLink() { 49 | const defaultLink = new dia.Link({ 50 | router: {name: 'manhattan'}, 51 | connector: {name: 'rounded'}, 52 | attrs: {'.marker-target': {d: 'M 10 0 L 0 5 L 10 10 z'}}, 53 | }); 54 | 55 | defaultLink.appendLabel({ 56 | attrs: { 57 | text: { 58 | text: 'REF' 59 | } 60 | }, 61 | position: { 62 | offset: 20 63 | } 64 | }); 65 | 66 | return defaultLink; 67 | }; 68 | 69 | /** 70 | * Creates a new link for the given source and target elements. The new link connects two ports: 71 | * sourcePort and targetPort 72 | * 73 | * @param {string} sourceId 74 | * @param {string} sourcePort 75 | * @param {string} targetId 76 | * @param {string} targetPort 77 | * @param {string} label 78 | * @return {dia.Link} a new link 79 | */ 80 | export const createLink = function createLink(sourceId, sourcePort, targetId, targetPort, labelText, cardinalityLabel) { 81 | const link = createDefaultLink() 82 | .router({name: 'manhattan'}) 83 | .connector({name: 'rounded'}) 84 | .source({ 85 | id: sourceId, 86 | port: sourcePort 87 | }) 88 | .target({ 89 | id: targetId, 90 | port: targetPort 91 | }); 92 | 93 | if (cardinalityLabel != null) link.appendLabel(cardinalityLabel); 94 | 95 | return link; 96 | }; 97 | 98 | /** 99 | * 100 | * @returns {linkTools.Button} 101 | */ 102 | const createInfoButton = function createInfoButton() { 103 | return new linkTools.Button({ 104 | markup: [{ 105 | tagName: 'circle', 106 | selector: 'button', 107 | attributes: { 108 | 'r': 11, 109 | 'fill': '#0099ee', 110 | 'cursor': 'pointer' 111 | } 112 | }, { 113 | tagName: 'path', 114 | selector: 'icon', 115 | attributes: { 116 | 'd': 'M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4', 117 | 'fill': 'none', 118 | 'stroke': '#FFFFFF', 119 | 'stroke-width': 2, 120 | 'pointer-events': 'none' 121 | } 122 | }], 123 | distance: -60, 124 | offset: 0, 125 | action: function () { 126 | cardinalityUpdateModal.modal('show'); 127 | cardinalitySelection.val('0..N'); 128 | cardinalityUpdateButton.off('click'); 129 | cardinalityUpdateButton.on('click', () => { 130 | let cardinalityLabel = { 131 | attrs: { 132 | text: { 133 | text: cardinalitySelection.children('option:selected').val() || '0..N' 134 | }, 135 | line: { 136 | strokeWidth: 2, 137 | } 138 | }, 139 | position: { 140 | distance: 20, 141 | offset: 20 142 | } 143 | }; 144 | this.model.appendLabel(cardinalityLabel); 145 | cardinalityUpdateModal.modal('hide'); 146 | }); 147 | } 148 | }); 149 | }; 150 | 151 | function validateConnection(cellViewS, magnetS, cellViewT, magnetT, end, linkView) { 152 | // if (!linkView.hasTools()) linkView.addTools(new dia.ToolsView({tools: [createInfoButton()]})); 153 | 154 | // Prevent linking from input ports. 155 | // if (magnetS && magnetS.getAttribute('port-group') === 'in') return false; 156 | 157 | // Prevent linking from output ports to input ports within one element. 158 | // if (cellViewS === cellViewT) return false; 159 | 160 | // Prevent linking to input ports. 161 | return magnetT; // && magnetT.getAttribute('port-group') === 'in'; 162 | } 163 | 164 | function zoomOnMousewheel(paper, delta) { 165 | if (isNull(paper) || isUndefined(paper)) return; 166 | 167 | const scale = paper.scale(); 168 | const newScaleX = scale.sx + (delta * 0.01); 169 | const newScaleY = scale.sy + (delta * 0.01); 170 | if (newScaleX >= 0.2 && newScaleX <= 2) paper.scale(newScaleX, newScaleY); 171 | } 172 | 173 | export const createPaper = function createPaper(paperDivElement, graph) { 174 | const paper = new dia.Paper({ 175 | gridSize: 1, 176 | width: '100%', 177 | height: '100%', 178 | markAvailable: true, 179 | restrictTranslate: true, 180 | model: graph, 181 | el: paperDivElement, 182 | cellViewNamespace: shapes, 183 | snapLinks: {radius: 75}, 184 | defaultAnchor: {name: 'center'}, 185 | defaultConnectionPoint: {name: 'anchor'}, 186 | defaultLink: createDefaultLink(), 187 | validateConnection: validateConnection, 188 | }); 189 | 190 | paper.on({ 191 | 'blank:mousewheel': (event, x, y, delta) => { 192 | event.preventDefault(); 193 | zoomOnMousewheel(paper, delta); 194 | }, 195 | 'cell:mousewheel': (_, event, x, y, delta) => { 196 | event.preventDefault(); 197 | zoomOnMousewheel(paper, delta); 198 | }, 199 | 'link:pointerup': (linkView) => { 200 | if (linkView.hasTools()) return; 201 | linkView.addTools(new dia.ToolsView({tools: [createInfoButton()]})); 202 | }, 203 | 'link:mouseenter': (linkView) => { 204 | linkView.showTools(); 205 | }, 206 | 'link:mouseleave': (linkView) => { 207 | linkView.hideTools(); 208 | }, 209 | 'element:collapse': (elementView, evt) => { 210 | evt.stopPropagation(); 211 | console.log('element:collapse'); 212 | }, 213 | 'element:mouseover': (elementView) => { 214 | // if (elementView instanceof DiagramRoot.ElementView) // console.log(elementView); 215 | } 216 | }); 217 | 218 | paper.scale(0.7, 0.7); 219 | 220 | graph.on('add', function (cell) { 221 | if (cell instanceof DiagramRoot.Element) { 222 | cell.findView(paper).addTools(new dia.ToolsView({ 223 | tools: [ 224 | new elementTools.Remove({ 225 | x: 425, 226 | y: -17.5, 227 | markup: [{ 228 | tagName: 'circle', 229 | selector: 'button', 230 | attributes: { 231 | 'r': 11, 232 | 'fill': '#FF1D00', 233 | 'cursor': 'pointer' 234 | } 235 | }, { 236 | tagName: 'path', 237 | selector: 'icon', 238 | attributes: { 239 | 'd': 'M -5 -5 5 5 M -5 5 5 -5', 240 | 'fill': 'none', 241 | 'stroke': '#FFFFFF', 242 | 'stroke-width': 3, 243 | 'pointer-events': 'none' 244 | } 245 | }] 246 | }), 247 | new elementTools.Button( 248 | { 249 | x: 425, 250 | y: 17.5, 251 | markup: [{ 252 | tagName: 'circle', 253 | selector: 'button', 254 | attributes: { 255 | 'r': 11, 256 | 'fill': '#0099ee', 257 | 'cursor': 'pointer' 258 | } 259 | }, { 260 | tagName: 'path', 261 | selector: 'icon', 262 | attributes: { 263 | 'd': 'M -2 4 2 4 M 0 3 0 0 M -2 -1 1 -1 M -1 -4 1 -4', 264 | 'fill': 'none', 265 | 'stroke': '#FFFFFF', 266 | 'stroke-width': 2, 267 | 'pointer-events': 'none' 268 | } 269 | }], 270 | action: function () { 271 | if(!isFunction(this.model.getSchema)) return; 272 | openSchemaUpdateModal(this.model); 273 | } 274 | } 275 | ) 276 | ] 277 | })); 278 | } 279 | }); 280 | 281 | return paper; 282 | }; 283 | 284 | export const createTitleRow = function createTitleRow(options) { 285 | return new DiagramTitle.Element({ 286 | customAttrs: { 287 | entity_title: options.title 288 | }, 289 | position: getPosition(options), 290 | size: getSize(options) 291 | }); 292 | }; 293 | 294 | export const createSimpleRow = function createSimpleRow(options) { 295 | return new SimpleRow.Element({ 296 | customAttrs: { 297 | field_name: options.field_name, 298 | field_constraints: options.field_constraints || 'ID, req, unq, idx', 299 | field_date_type: options.field_date_type || 'str' 300 | }, 301 | size: {width: options.width || 0, height: options.height || 0}, 302 | position: {x: options.x || 0, y: options.y || 0}, 303 | rowLevel: options.rowLevel || 0, 304 | inPorts: ['in'], 305 | outPorts: ['out'], 306 | ports: PORT_OPTIONS 307 | }); 308 | }; 309 | 310 | export const createObjectRow = function createObjectRow(options) { 311 | const objectRowHeader = new ObjectRowHeader.Element({ 312 | customAttrs: { 313 | field_name: options.field_name || 'new_field', 314 | field_constraints: options.field_constraints || 'ID, req, unq, idx', 315 | field_date_type: options.field_date_type || 'obj' 316 | }, 317 | rowLevel: options.rowLevel || 0, 318 | size: {width: options.width || 0, height: options.height || 0}, 319 | position: {x: options.x || 0, y: options.y || 0}, 320 | inPorts: ['in'], 321 | outPorts: ['out'], 322 | ports: PORT_OPTIONS 323 | }); 324 | 325 | const objectRow = new ObjectRow.Element({ 326 | attrs: { 327 | text: {text: options.field_name || 'new_object_row'} 328 | }, 329 | isObjectRow: true, 330 | size: {width: options.width || 0, height: options.height || 0}, 331 | position: {x: options.x || 0, y: options.y || 0}, 332 | rowLevel: options.rowLevel || 0, 333 | }); 334 | 335 | objectRow.prop('objectRowHeader', objectRowHeader); 336 | 337 | return objectRow; 338 | }; 339 | 340 | export const createRect = function () { 341 | return new shapes.devs.Model(); 342 | }; 343 | -------------------------------------------------------------------------------- /src/js/jointjs-helper/template-generator.js: -------------------------------------------------------------------------------- 1 | import _templateSettings from 'lodash/templateSettings'; 2 | 3 | _templateSettings.interpolate = /\{\{(.+?)\}\}/g; 4 | var tmpl = _.template('
  • {{ name }}
  • '); 5 | 6 | const names = ["Alice", "Bob", "Dave", "Jane"]; 7 | 8 | const LiList = names.map(i => tmpl({name: i})); 9 | 10 | const ulElem = [].concat("").join(""); 11 | 12 | export default ulElem; -------------------------------------------------------------------------------- /src/js/json-editor/json-editor.js: -------------------------------------------------------------------------------- 1 | import JSONEditor from "jsoneditor"; 2 | import $ from 'jquery'; 3 | import isNil from 'lodash/isNil'; 4 | import {json as schemaGenerator} from 'generate-schema'; 5 | import {createDiagramRoot, updateDiagramRoot} from '../jointjs-helper/diagram-generator'; 6 | import {jsonDocValidator, jsonSchemaValidator} from './schema-validators'; 7 | import {schema as bookSchema} from "../jointjs-helper/schema-examples"; 8 | 9 | const jsonEditorModal = $('#jsonEditorModal'); 10 | const modalTitle = $('#jsonEditorModal .modal-title'); 11 | const actionButton = $('#json-editor-action-btn'); 12 | const entityTypeNameInput = $('#entity-type-name-input'); 13 | const invalidFeedbackBlock = $('#entity-type-name-form .invalid-feedback'); 14 | actionButton.off('click'); 15 | 16 | const jsonEditor = createJSONEditor( 17 | document.getElementById("json-editor"), 18 | { 19 | mode: 'code', 20 | search: false, 21 | statusBar: true, 22 | enableTransform: false, 23 | history: true, 24 | schema: jsonDocValidator, 25 | onError: onError, 26 | onChange: onJsonDocChange 27 | }, 28 | {} 29 | ); 30 | 31 | /** 32 | * 33 | * @param {string | Object} err 34 | */ 35 | function onError(err) { 36 | console.log(err); 37 | } 38 | 39 | /** 40 | * Gets triggered on user interactions with JsonEditor. If the entered json document 41 | * isn't valid against the pre defined schema, then will be an error thrown and the 42 | * button 'Visualize' will be disabled. The button will be also disable, if there 43 | * occurs an error. 44 | */ 45 | function onJsonDocChange() { 46 | try { 47 | if (!!jsonEditor) { 48 | const json = jsonEditor.get(); 49 | if (jsonEditor.validateSchema(json) && isEntityTypeNameValid()) { 50 | actionButton.prop("disabled", false); 51 | } else { 52 | showErrorsTable(); 53 | actionButton.prop("disabled", true); 54 | } 55 | } 56 | } catch (e) { 57 | actionButton.prop("disabled", true); 58 | } 59 | } 60 | 61 | jsonEditorModal.on('shown.bs.modal', () => { 62 | $('button.jsoneditor-format').trigger('click'); 63 | // onJsonDocChange(); 64 | }); 65 | 66 | jsonEditorModal.on('hidden.bs.modal', () => { 67 | entityTypeNameInput.val(''); 68 | entityTypeNameInput.removeClass('is-valid').addClass('is-invalid'); 69 | 70 | }); 71 | 72 | function showErrorsTable() { 73 | const errorIcon = $('.jsoneditor-validation-error-icon'); 74 | if (errorIcon.css('display') === 'none') { 75 | return; 76 | } 77 | 78 | const errorsTable = $('.jsoneditor-validation-errors'); 79 | if (errorsTable.length > 0) { 80 | errorIcon.trigger('click'); 81 | } 82 | 83 | errorIcon.trigger('click'); 84 | } 85 | 86 | 87 | /** 88 | * Create a new JSONEditor 89 | * @param {string | Element} selector A query selector id like '#myEditor' 90 | * or a DOM element to use as container 91 | * @param {Object} [options] 92 | * @param {Object} [json] 93 | * @return {JSONEditor} Returns the created JSONEditor 94 | */ 95 | function createJSONEditor(selector, options, json) { 96 | const container = (typeof selector === 'string') 97 | ? document.querySelector(selector) 98 | : selector; 99 | 100 | const editor = new JSONEditor(container, options, json); 101 | container.jsoneditor = editor; 102 | return editor; 103 | } 104 | 105 | $('#example1').on('click', () => { 106 | createDiagramRoot(bookSchema); 107 | }); 108 | 109 | $('#json-doc-button').on('click', () => { 110 | jsonEditor.set({}); 111 | jsonEditor.setSchema(jsonDocValidator); 112 | 113 | actionButton.off('click'); 114 | actionButton.on('click', () => { 115 | if (isNil(jsonEditor)) return; 116 | try { 117 | let inferredSchema = schemaGenerator(jsonEditor.get()); 118 | if(inferredSchema['type'] === 'array') inferredSchema = inferredSchema['items']; 119 | inferredSchema["title"] = entityTypeNameInput.val() || ""; 120 | createDiagramRoot(inferredSchema); 121 | jsonEditorModal.modal('hide'); 122 | } catch (err) { 123 | console.log(err); 124 | } 125 | }); 126 | 127 | modalTitle.text('Import a JSON Document'); 128 | actionButton.text('Visualize'); 129 | jsonEditorModal.modal('show'); 130 | }); 131 | 132 | $('#json-schema-button').on('click', () => { 133 | jsonEditor.set({}); 134 | jsonEditor.setSchema(jsonSchemaValidator); 135 | 136 | actionButton.off('click'); 137 | actionButton.on('click', () => { 138 | if (isNil(jsonEditor)) return; 139 | try { 140 | const doc = jsonEditor.get(); 141 | doc["title"] = entityTypeNameInput.val() || ""; 142 | createDiagramRoot(doc); 143 | jsonEditorModal.modal('hide'); 144 | } catch (err) { 145 | console.log(err); 146 | } 147 | }); 148 | 149 | modalTitle.text('Import a JSON-Schema'); 150 | actionButton.text('Visualize'); 151 | jsonEditorModal.modal('show'); 152 | }); 153 | 154 | function isEntityTypeNameValid() { 155 | return (/^([a-zA-Z0-9_]+)$/.test(entityTypeNameInput.val())); 156 | } 157 | 158 | entityTypeNameInput.on('input propertychange', function () { 159 | const isValid = isEntityTypeNameValid(); 160 | if (isValid) { 161 | invalidFeedbackBlock.fadeOut(); 162 | entityTypeNameInput.removeClass('is-invalid').addClass('is-valid'); 163 | // actionButton.prop("disabled", false); 164 | onJsonDocChange(); 165 | } else { 166 | invalidFeedbackBlock.fadeIn(); 167 | entityTypeNameInput.removeClass('is-valid').addClass('is-invalid'); 168 | actionButton.prop("disabled", true); 169 | } 170 | }); 171 | 172 | export const openSchemaUpdateModal = function (diagramRoot) { 173 | const diagramRootSchema = diagramRoot.getSchema(); 174 | 175 | jsonEditor.set(diagramRootSchema); 176 | jsonEditor.setSchema(jsonSchemaValidator); 177 | 178 | entityTypeNameInput.val(diagramRootSchema["title"] || "TITLE"); 179 | entityTypeNameInput.removeClass('is-invalid').addClass('is-valid'); 180 | 181 | modalTitle.text('Update the Schema'); 182 | actionButton.text('Update'); 183 | 184 | actionButton.off('click'); 185 | actionButton.on('click', () => { 186 | if (isNil(jsonEditor)) return; 187 | try { 188 | const updatedSchema = jsonEditor.get(); 189 | updatedSchema["title"] = entityTypeNameInput.val() || ""; 190 | diagramRoot.setSchema(updatedSchema); 191 | 192 | updateDiagramRoot(diagramRoot); 193 | jsonEditorModal.modal('hide'); 194 | } catch (err) { 195 | console.log(err); 196 | } 197 | }); 198 | jsonEditorModal.modal('show'); 199 | }; -------------------------------------------------------------------------------- /src/js/json-editor/schema-validators.js: -------------------------------------------------------------------------------- 1 | export const jsonDocValidator = { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "anyOf": [ 4 | {"$ref": "#/definitions/object_typ"}, 5 | {"$ref": "#/definitions/array_type"} 6 | ], 7 | "definitions": { 8 | "object_typ": { 9 | "type": "object", 10 | "minProperties": 1 11 | }, 12 | "array_type": { 13 | "type": "array", 14 | "items": {"$ref": "#/definitions/object_typ"}, 15 | "minItems": 1 16 | } 17 | } 18 | }; 19 | 20 | export const jsonSchemaValidator = { 21 | "title": "JSON-Schema-Validator", 22 | "description": "Validiert das von Benutzer eingegebebes JSON-Schema", 23 | "type": "object", 24 | "properties": { 25 | "title": { 26 | "type": "string" 27 | }, 28 | "properties": { 29 | "type": "object", 30 | "minProperties": 1, 31 | "additionalProperties": { 32 | "anyOf": [ 33 | {"$ref": "#/definitions/primitive_types"}, 34 | {"$ref": "#/definitions/array_type"}, 35 | {"$ref": "#/definitions/object_type"} 36 | ] 37 | } 38 | } 39 | }, 40 | "required": ["properties"], 41 | "definitions": { 42 | "primitive_types": { 43 | "type": "object", 44 | "description": "Validiert primitive Datentypen, wie string, number und boolean", 45 | "properties": { 46 | "type": {"type": "string"}, 47 | }, 48 | "required": ["type"], 49 | "additionalProperties": false 50 | }, 51 | "array_type": { 52 | "type": "object", 53 | "properties": { 54 | "type": {"type": "string"}, 55 | "items": { 56 | "anyOf": [ 57 | {"$ref": "#/definitions/primitive_types"}, 58 | {"$ref": "#/definitions/items"} 59 | ] 60 | } 61 | }, 62 | "required": ["type", "items"], 63 | "additionalProperties": false 64 | }, 65 | "object_type": { 66 | "type": "object", 67 | "properties": { 68 | "type": {"type": "string"}, 69 | "properties": { 70 | "type": "object", 71 | "additionalProperties": { 72 | "anyOf": [ 73 | {"$ref": "#/definitions/primitive_types"}, 74 | {"$ref": "#/definitions/object_type"}, 75 | {"$ref": "#/definitions/array_type"} 76 | ] 77 | } 78 | } 79 | }, 80 | "required": ["type", "properties"] 81 | }, 82 | "items": { 83 | "type": "array", 84 | "items": { 85 | "anyOf": [ 86 | {"$ref": "#/definitions/primitive_types"}, 87 | {"$ref": "#/definitions/object_type"}, 88 | {"$ref": "#/definitions/array_type"} 89 | ] 90 | } 91 | } 92 | } 93 | }; -------------------------------------------------------------------------------- /src/js/modal-template.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/schema-diagram/common/hierarchy-base-view.js: -------------------------------------------------------------------------------- 1 | import {dia} from 'jointjs'; 2 | import HierarchyBase from "./hierarchy-base"; 3 | import {renderBox, updateBox, removeBox, initializeBox} from '../utils'; 4 | 5 | HierarchyBase.ElementView = dia.ElementView.extend({ 6 | initialize: initializeBox, 7 | render: renderBox, 8 | updateBox: updateBox, 9 | removeBox: removeBox 10 | }); 11 | 12 | export default HierarchyBase; -------------------------------------------------------------------------------- /src/js/schema-diagram/common/hierarchy-base.js: -------------------------------------------------------------------------------- 1 | import _isUndefined from 'lodash/isUndefined'; 2 | import {shapes, util} from 'jointjs'; 3 | 4 | const HierarchyBase = shapes.html.HierarchyBase = {}; 5 | /** 6 | * typedef {joint.devs.Couple} HierarchyBase.Element 7 | */ 8 | 9 | /** 10 | * @class 11 | * @extends shapes.devs.Coupled 12 | */ 13 | HierarchyBase.Element = shapes.devs.Coupled.extend((function () { 14 | const model = this; 15 | 16 | /** 17 | * Default attributes 18 | * @type {object} 19 | */ 20 | let defaults = util.defaultsDeep({ 21 | type: 'html.HierarchyBase.Element', 22 | attrs: { 23 | '.body': {stroke: '#ffffff'}, 24 | }, 25 | }, shapes.devs.Coupled.prototype.defaults); 26 | 27 | /** 28 | * 29 | * @param {DiagramTitle.Element} title 30 | */ 31 | const setDiagramTitle = function (title) { 32 | this.prop('diagramTitle', title); 33 | }; 34 | 35 | const getDiagramTitle = function () { 36 | return this.prop('diagramTitle'); 37 | }; 38 | 39 | /** 40 | * 41 | * @param {SimpleRow.Element} row 42 | */ 43 | const addSimpleRow = function (row) { 44 | if (_isUndefined(this.get('simpleRowList'))) this.prop('simpleRowList', []); 45 | 46 | this.get('simpleRowList').push(row); 47 | }; 48 | 49 | /** 50 | * 51 | * @param {ObjectRow.Element} row 52 | */ 53 | const addObjectRow = function (row) { 54 | if (_isUndefined(this.get('objectRowList'))) this.prop('objectRowList', []); 55 | 56 | this.get('objectRowList').push(row); 57 | }; 58 | 59 | const getSimpleRowList = function () { 60 | return this.get('simpleRowList'); 61 | }; 62 | 63 | const getObjectRowList = function () { 64 | return this.get('objectRowList'); 65 | }; 66 | 67 | const simpleRowListLength = function () { 68 | return this.get('simpleRowList').length; 69 | }; 70 | 71 | const objectRowListLength = function () { 72 | return this.get('objectRowList').length; 73 | }; 74 | 75 | const removeChildCells = function () { 76 | this.prop('simpleRowList', []); 77 | this.prop('objectRowList', []); 78 | }; 79 | 80 | /** 81 | * TODO: create the following attributes and their getter/setter methods 82 | * - arrayRowList 83 | * - multipleTypeRowList 84 | * 85 | * TODO: create the following methods 86 | * - removeSimpleRow: finds the row by id and removes it from the list 'simpleRowList' 87 | * - removeObjectRow: same as removeSimpleRow for objectRows 88 | */ 89 | 90 | return { 91 | defaults: defaults, 92 | addSimpleRow: addSimpleRow, 93 | addObjectRow: addObjectRow, 94 | setDiagramTitle: setDiagramTitle, 95 | getDiagramTitle: getDiagramTitle, 96 | getSimpleRowList: getSimpleRowList, 97 | getObjectRowList: getObjectRowList, 98 | removeChildCells: removeChildCells 99 | }; 100 | })()); 101 | 102 | export default HierarchyBase; -------------------------------------------------------------------------------- /src/js/schema-diagram/common/html-element.js: -------------------------------------------------------------------------------- 1 | import _isUndefined from 'lodash/isUndefined'; 2 | import { dia, shapes, util } from 'jointjs'; 3 | import {renderBox, updateBox, removeBox, initializeBox, appendValuesToTemplate} from '../utils'; 4 | 5 | if (_isUndefined(shapes.html)) { 6 | shapes.html = {}; 7 | } 8 | 9 | const CustomHtml = shapes.html; 10 | 11 | CustomHtml.Element = shapes.devs.Atomic.extend({ 12 | defaults: util.defaultsDeep({ 13 | type: 'html.Element', 14 | attrs: { 15 | '.body': { stroke: '#ffffff' } 16 | } 17 | }, shapes.devs.Atomic.prototype.defaults), 18 | rowLevel: 0 19 | }); 20 | 21 | CustomHtml.ElementView = dia.ElementView.extend({ 22 | htmlTemplate: '', 23 | initialize: initializeBox, 24 | addAdditionalEvents: function() {}, 25 | appendValuesToTemplate: appendValuesToTemplate, 26 | render: renderBox, 27 | updateBox: updateBox, 28 | removeBox: removeBox 29 | }); 30 | 31 | export default CustomHtml; -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-root/diagram-root-view.js: -------------------------------------------------------------------------------- 1 | import {renderBox, updateBox, removeBox, initializeBox} from '../utils'; 2 | import DiagramRoot from "./diagram-root"; 3 | import HierarchyBase from "../common/hierarchy-base-view"; 4 | 5 | 6 | DiagramRoot.ElementView = HierarchyBase.ElementView.extend({ 7 | initialize: initializeBox, 8 | render: renderBox, 9 | updateBox: updateBox, 10 | removeBox: removeBox 11 | }); 12 | 13 | export default DiagramRoot; 14 | -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-root/diagram-root.html: -------------------------------------------------------------------------------- 1 |
    -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-root/diagram-root.js: -------------------------------------------------------------------------------- 1 | import {shapes, util} from 'jointjs'; 2 | import isNil from 'lodash/isNil'; 3 | import HierarchyBase from "../common/hierarchy-base"; 4 | 5 | const DiagramRoot = shapes.html.DiagramRoot = {}; 6 | 7 | DiagramRoot.Element = HierarchyBase.Element.extend((function () { 8 | const model = this; 9 | 10 | /** 11 | * Default attributes 12 | * @type {object} 13 | */ 14 | let defaults = util.defaultsDeep({ 15 | type: 'html.DiagramRoot.Element', 16 | attrs: { 17 | '.body': {stroke: '#ffffff'}, 18 | }, 19 | }, HierarchyBase.Element.prototype.defaults); 20 | 21 | const setSchema = function (schema) { 22 | this.prop('schema', schema); 23 | }; 24 | 25 | const getSchema = function () { 26 | const schema = this.get('schema'); 27 | 28 | if (isNil(schema)) return {}; 29 | return schema; 30 | }; 31 | 32 | return { 33 | defaults: defaults, 34 | setSchema: setSchema, 35 | getSchema: getSchema, 36 | }; 37 | })()); 38 | 39 | export default DiagramRoot; -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-root/index.js: -------------------------------------------------------------------------------- 1 | import DiagramRoot from "./diagram-root-view"; 2 | 3 | export default DiagramRoot; -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-title/diagram-title.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 |
    6 |
    7 |
    -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-title/diagram-title.js: -------------------------------------------------------------------------------- 1 | import _isUndefined from 'lodash/isUndefined'; 2 | import { shapes, util } from 'jointjs'; 3 | import CustomHtml from '../common/html-element'; 4 | import DiagramTitleTemplate from './diagram-title.html'; 5 | 6 | if (_isUndefined(shapes.html)) { 7 | throw Error("joint.shapes.html is not undefined"); 8 | } 9 | 10 | const DiagramTitle = shapes.html.DiagramTitle = {}; 11 | 12 | DiagramTitle.Element = CustomHtml.Element.extend({ 13 | defaults: util.defaultsDeep({ 14 | type: 'html.DiagramTitle.Element', 15 | }, CustomHtml.Element.prototype.defaults) 16 | }); 17 | 18 | DiagramTitle.ElementView = CustomHtml.ElementView.extend({ 19 | htmlTemplate: DiagramTitleTemplate, 20 | }); 21 | 22 | export default DiagramTitle; -------------------------------------------------------------------------------- /src/js/schema-diagram/diagram-title/index.js: -------------------------------------------------------------------------------- 1 | import DiagramTitle from "./diagram-title"; 2 | 3 | export default DiagramTitle; -------------------------------------------------------------------------------- /src/js/schema-diagram/object-row-header/object-row-header.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <% for(var i = 0; i < rowLevel; i++) { %> 5 |
    6 | <% } %> 7 |
    8 | 9 |
    10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    -------------------------------------------------------------------------------- /src/js/schema-diagram/object-row-header/object-row-header.js: -------------------------------------------------------------------------------- 1 | import _isUndefined from 'lodash/isUndefined'; 2 | import _isNil from 'lodash/isNil'; 3 | import _isFunction from 'lodash/isFunction'; 4 | import _has from 'lodash/has'; 5 | import _forEach from 'lodash/forEach'; 6 | import {shapes, util} from 'jointjs'; 7 | import CustomHtml from '../common/html-element'; 8 | import ObjectRowHeaderTemplate from './object-row-header.html'; 9 | import {appendValuesToTemplate} from "../utils"; 10 | import ObjectRow from "../object-row/object-row"; 11 | import DiagramRoot from "../diagram-root"; 12 | 13 | const HEIGHT = 35; 14 | 15 | if (_isUndefined(shapes.html)) { 16 | throw Error("joint.shapes.html is not undefined"); 17 | } 18 | 19 | const ObjectRowHeader = shapes.html.ObjectRowHeader = {}; 20 | 21 | ObjectRowHeader.Element = CustomHtml.Element.extend({ 22 | defaults: util.defaultsDeep({ 23 | type: 'html.ObjectRowHeader.Element', 24 | }, CustomHtml.Element.prototype.defaults) 25 | }); 26 | 27 | ObjectRowHeader.ElementView = CustomHtml.ElementView.extend({ 28 | isCollapsed: true, 29 | 30 | htmlTemplate: ObjectRowHeaderTemplate, 31 | 32 | appendValuesToTemplate: appendValuesToTemplate, 33 | 34 | addAdditionalEvents: function () { 35 | if (_isUndefined(this)) return; 36 | if (!_has(this, 'model')) return; 37 | if (!_has(this, 'model.graph')) return; 38 | if (!_has(this, '$box')) return; 39 | 40 | const view = this; 41 | const model = view.model; 42 | const graph = view.model.graph; 43 | const $box = view.$box; 44 | let parentCell; 45 | 46 | const rowExpander = $box.find('.row-expander'); 47 | if (_isUndefined(rowExpander)) return; 48 | 49 | const caretRight = $box.find('.fa-caret-right'); 50 | if (_isUndefined(caretRight)) return; 51 | 52 | rowExpander.on('click', (evt) => { 53 | if (_isNil(parentCell)) parentCell = model.getParentCell(); 54 | caretRight.toggleClass('down'); 55 | 56 | if (parentCell.isCollapsed()) { 57 | expandParentRow(parentCell); 58 | } else if (!parentCell.isCollapsed()) { 59 | collapseParentRow(model, parentCell); 60 | } 61 | }); 62 | } 63 | }); 64 | 65 | function expandParentRow(cell) { 66 | if (_isNil(cell)) return; 67 | if (!_has(cell, 'graph')) return; 68 | if (!_isFunction(cell.getParentCell)) return; 69 | 70 | moveSimpleRows(cell); 71 | moveObjectRows(cell); 72 | 73 | adjustCell(cell); 74 | 75 | cell.setExpanded(); 76 | } 77 | 78 | function collapseParentRow(cell, parentCell) { 79 | const deepEmbeds = parentCell.getEmbeddedCells({deep: true}).filter((item) => item !== cell); 80 | _forEach(deepEmbeds, (cell) => {if (cell instanceof ObjectRow.Element) { cell.setCollapsed();}}); 81 | 82 | parentCell.graph.removeCells(deepEmbeds); 83 | parentCell.fitEmbeds(); 84 | parentCell.setCollapsed(); 85 | adjustCell(parentCell); 86 | } 87 | 88 | /** 89 | * 90 | * @param cell JointJs Element 91 | */ 92 | function adjustCell(cell) { 93 | const parentCell = cell.getParentCell(); 94 | const objectRowList = parentCell.getObjectRowList(); 95 | 96 | let cellPosition = objectRowList[0].prop("position"); 97 | let cellHeight = objectRowList[0].prop("size/height"); 98 | let nextPositionY = cellPosition.y + cellHeight; 99 | 100 | _forEach(objectRowList.slice(1), (objectRow, index) => { 101 | objectRow.position(cellPosition.x, nextPositionY, {deep: true}); 102 | nextPositionY = nextPositionY + objectRow.prop("size/height"); 103 | }); 104 | 105 | parentCell.fitEmbeds(); 106 | 107 | if(parentCell instanceof DiagramRoot.Element) return; 108 | adjustCell(parentCell); 109 | } 110 | 111 | /** 112 | * 113 | * @param cell JointJs Element 114 | */ 115 | function moveSimpleRows(cell) { 116 | if (_isNil(cell)) return; 117 | if (_isNil(cell.getSimpleRowList)) return; 118 | if (_isNil(cell.fitEmbeds)) return; 119 | 120 | const graph = cell.graph; 121 | const modelHeight = cell.prop('size/height'); 122 | 123 | _forEach(cell.getSimpleRowList(), (simpleRow, index) => { 124 | simpleRow.prop('z', cell.prop('z')); 125 | graph.addCell(simpleRow); 126 | cell.embed(simpleRow); 127 | simpleRow.position(0, modelHeight + (index * HEIGHT), {parentRelative: true}); 128 | }); 129 | cell.fitEmbeds(); 130 | } 131 | 132 | /** 133 | * 134 | * @param cell JointJs Element 135 | */ 136 | function moveObjectRows(cell) { 137 | if (_isNil(cell)) return; 138 | if (_isNil(cell.getObjectRowList)) return; 139 | if (_isNil(cell.fitEmbeds)) return; 140 | 141 | const graph = cell.graph; 142 | const modelHeight = cell.prop('size/height'); 143 | 144 | _forEach(cell.getObjectRowList(), (objectRow, index) => { 145 | const header = objectRow.getHeader(); 146 | graph.addCell(header); 147 | objectRow.embed(header); 148 | 149 | graph.addCell(objectRow); 150 | cell.embed(objectRow); 151 | 152 | objectRow.position(0, modelHeight + (index * HEIGHT), {parentRelative: true}); 153 | header.position(0, 0, {parentRelative: true}); 154 | 155 | objectRow.fitEmbeds(); 156 | }); 157 | 158 | cell.fitEmbeds(); 159 | } 160 | 161 | export default ObjectRowHeader; 162 | -------------------------------------------------------------------------------- /src/js/schema-diagram/object-row/object-row.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <% for(var i = 0; i < rowLevel; i++) { %> 5 |
    6 | <% } %> 7 |
    8 | 9 |
    10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    -------------------------------------------------------------------------------- /src/js/schema-diagram/object-row/object-row.js: -------------------------------------------------------------------------------- 1 | import _findIndex from 'lodash/findIndex'; 2 | import _isUndefined from 'lodash/isUndefined'; 3 | import _forEach from 'lodash/forEach'; 4 | import {shapes, util} from 'jointjs'; 5 | import HierarchyBase from "../common/hierarchy-base-view"; 6 | import {appendValuesToTemplate} from "../utils"; 7 | import SimpleRow from "../simple-row/simple-row"; 8 | 9 | 10 | if (_isUndefined(shapes.html)) { 11 | throw Error("joint.shapes.html is not undefined"); 12 | } 13 | 14 | const HEIGHT = 35; 15 | const TRANSITION_DELAY = 0; 16 | const TRANSITION_DURATION = 100; 17 | 18 | const ObjectRow = shapes.html.ObjectRow = {}; 19 | 20 | /** 21 | * typedef {HierarchyBase.Element} ObjectRow.Element 22 | */ 23 | 24 | 25 | ObjectRow.Element = HierarchyBase.Element.extend((function () { 26 | /** 27 | * Model defaults 28 | */ 29 | const defaults = util.defaultsDeep({ 30 | type: 'html.ObjectRow.Element', 31 | }, HierarchyBase.Element.prototype.defaults); 32 | 33 | let rowLevel = 0; 34 | 35 | /** 36 | * Sign the undefined value to prevent the inheritance 37 | * @type {undefined} 38 | */ 39 | let diagramTitle = undefined; 40 | 41 | /** 42 | * Sign the undefined value to prevent the inheritance 43 | * @type {undefined} 44 | */ 45 | const setDiagramTitle = undefined; 46 | 47 | /** 48 | * 49 | * @param {SimpleRow.Element} header 50 | */ 51 | const setHeader = function (header) { 52 | if(_isUndefined(this.get('objectRowHeader'))) this.prop('objectRowHeader', header); 53 | }; 54 | 55 | const getHeader = function () { 56 | return (_isUndefined(this.get('objectRowHeader'))) ? new SimpleRow.Element() : this.get('objectRowHeader'); 57 | }; 58 | 59 | const isCollapsed = function () { 60 | if (_isUndefined(this.get('isCollapsed'))) this.prop('isCollapsed', true); 61 | return this.get('isCollapsed'); 62 | }; 63 | 64 | const setCollapsed = function () { 65 | this.prop('isCollapsed', true); 66 | }; 67 | 68 | const setExpanded = function() { 69 | this.prop('isCollapsed', false); 70 | }; 71 | 72 | return { 73 | defaults: defaults, 74 | rowLevel: rowLevel, 75 | setHeader: setHeader, 76 | getHeader: getHeader, 77 | isCollapsed: isCollapsed, 78 | setCollapsed: setCollapsed, 79 | setExpanded: setExpanded, 80 | }; 81 | })()); 82 | 83 | ObjectRow.ElementView = HierarchyBase.ElementView.extend({ 84 | // htmlTemplate: ObjectRowTemplate, 85 | isCollapsed: true, 86 | 87 | appendValuesToTemplate: appendValuesToTemplate, 88 | 89 | addAdditionalEvents_tmp: function () { 90 | const view = this; 91 | if (_isUndefined(view)) return; 92 | 93 | removeAllSimpleRows(view); 94 | 95 | let parentCell = view.model.getParentCell(); 96 | 97 | 98 | 99 | rowExpander.on('click', (evt) => { 100 | const model = view.model; 101 | const graph = model.graph; 102 | let modelHeight = model.prop('size/height'); 103 | 104 | // console.log(model.getSimpleRowList()); 105 | /* workaround to get the parent cell */ 106 | parentCell = parentCell || model.getParentCell(); 107 | 108 | caretRight.toggleClass('down'); 109 | 110 | if (view.isCollapsed) { 111 | // expandRow(view, parentCell); 112 | 113 | _forEach(model.getSimpleRowList(), (simpleRow, index) => { 114 | graph.addCell(simpleRow); 115 | model.embed(simpleRow); 116 | simpleRow.position(0, modelHeight + (index * 35), {parentRelative: true}); 117 | }); 118 | 119 | // model.fitEmbeds(); 120 | 121 | } else if (!view.isCollapsed) { 122 | // collapseRow(view, parentCell); 123 | } 124 | }); 125 | }, 126 | 127 | }); 128 | 129 | function expandRow(view, parentCell) { 130 | 131 | if (_isUndefined(view)) return; 132 | if (_isUndefined(parentCell)) return; 133 | 134 | const parentHeight = parentCell.prop("size/height"); 135 | 136 | let offset = HEIGHT * (view.model.simpleRowListLength() + view.model.objectRowListLength()); 137 | if (offset === 0) return; 138 | 139 | parentCell.transition("size/height", parentHeight + offset, { 140 | delay: TRANSITION_DELAY, 141 | duration: TRANSITION_DURATION 142 | }); 143 | 144 | const slicedEmbeds = sliceEmbeds(view.model, parentCell); 145 | slicedEmbeds.forEach((cell) => { 146 | let cellPositionY = cell.prop("position/y"); 147 | cell.transition("position/y", cellPositionY + offset, { 148 | delay: TRANSITION_DELAY, 149 | duration: TRANSITION_DURATION 150 | }); 151 | }); 152 | 153 | const modelPosition = view.model.get('position'); 154 | offset = 0; 155 | 156 | view.model.getSimpleRowList().forEach((simpleRow, index) => { 157 | offset = HEIGHT * (index + 1); 158 | simpleRow.position(modelPosition.x, modelPosition.y + offset); 159 | view.model.graph.addCell(simpleRow); 160 | parentCell.embed(simpleRow); 161 | }); 162 | 163 | view.model.getObjectRowList().forEach((objectRow, index) => { 164 | offset = HEIGHT * (index + 1); 165 | objectRow.position(modelPosition.x, modelPosition.y + offset); 166 | view.model.graph.addCell(objectRow); 167 | parentCell.embed(objectRow); 168 | }); 169 | 170 | view.isCollapsed = false; 171 | } 172 | 173 | function collapseRow(view, parentCell) { 174 | if (_isUndefined(view)) return; 175 | if (_isUndefined(parentCell)) return; 176 | 177 | const parentHeight = parentCell.prop("size/height"); 178 | if (parentHeight <= HEIGHT) return; 179 | 180 | removeAllSimpleRows(view); 181 | removeAllObjectRows(view); 182 | 183 | let offset = HEIGHT * (view.model.simpleRowListLength() + view.model.objectRowListLength()); 184 | if (offset === 0) return; 185 | 186 | parentCell.transition("size/height", parentHeight - offset, { 187 | delay: TRANSITION_DELAY, 188 | duration: TRANSITION_DURATION 189 | }); 190 | 191 | const slicedEmbeds = sliceEmbeds(view.model, parentCell); 192 | slicedEmbeds.forEach((cell) => { 193 | let cellPositionY = cell.prop("position/y"); 194 | cell.transition("position/y", cellPositionY - offset, { 195 | delay: TRANSITION_DELAY, 196 | duration: TRANSITION_DURATION 197 | }); 198 | }); 199 | 200 | view.isCollapsed = true; 201 | } 202 | 203 | function removeAllSimpleRows(view) { 204 | const graph = view.model.graph; 205 | graph.removeCells(view.model.simpleRowList); 206 | } 207 | 208 | function removeAllObjectRows(view) { 209 | const graph = view.model.graph; 210 | graph.removeCells(view.model.objectRowList); 211 | view.model.objectRowList.forEach((objectRow) => { 212 | removeObjectRow(view, objectRow); 213 | }); 214 | } 215 | 216 | function removeObjectRow(view, objectRow) { 217 | const graph = view.model.graph; 218 | graph.removeCells(objectRow.simpleRowList); 219 | graph.removeCells(objectRow.objectRowList); 220 | 221 | objectRow.objectRowList.forEach((row) => { 222 | removeObjectRow(view, row); 223 | }); 224 | } 225 | 226 | function sliceEmbeds(model, parentCell) { 227 | if (_isUndefined(model) || _isUndefined(parentCell)) return []; 228 | 229 | let embeddedCells = parentCell.getEmbeddedCells(); 230 | embeddedCells = embeddedCells.filter((cell) => _isUndefined(cell.prop("rowLevel")) || cell.prop("rowLevel") <= model.prop("rowLevel")); 231 | const modelCellIndex = _findIndex(embeddedCells, (cell) => cell.get("id") === model.get("id")); 232 | const slicedCells = embeddedCells.slice(modelCellIndex + 1); 233 | return slicedCells; 234 | } 235 | 236 | export default ObjectRow; 237 | 238 | /** 239 | * get all child nodes of the parent 240 | */ 241 | /* 242 | var subtree = []; 243 | function collectDeepEmbedded(cell) { 244 | _.each(cell.getEmbeddedCells(), function(c) { 245 | subtree.push(c); 246 | collectDeepEmbedded(c); 247 | }) 248 | } 249 | collectDeepEmbedded(myCell); 250 | */ 251 | -------------------------------------------------------------------------------- /src/js/schema-diagram/simple-row/simple-row.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | <% for(var i = 0; i < rowLevel; i++) { %> 5 |
    6 | <% } %> 7 | 8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    -------------------------------------------------------------------------------- /src/js/schema-diagram/simple-row/simple-row.js: -------------------------------------------------------------------------------- 1 | import { shapes, util } from 'jointjs'; 2 | import CustomHtml from '../common/html-element'; 3 | import SimpleRowTemplate from './simple-row.html'; 4 | 5 | /** 6 | * Use the new custom html element tutorial 7 | * 8 | * URL: https://github.com/clientIO/joint/blob/master/demo/shapes/src/html.js 9 | */ 10 | 11 | const SimpleRow = CustomHtml.SimpleRow = {}; 12 | SimpleRow.Element = shapes.html.Element.extend({ 13 | defaults: util.defaultsDeep({ 14 | type: 'html.SimpleRow.Element', 15 | }, CustomHtml.Element.prototype.defaults), 16 | }); 17 | 18 | SimpleRow.ElementView = CustomHtml.ElementView.extend({ 19 | htmlTemplate: SimpleRowTemplate 20 | }); 21 | 22 | export default SimpleRow; -------------------------------------------------------------------------------- /src/js/schema-diagram/supertype/supertype-view.js: -------------------------------------------------------------------------------- 1 | import {dia} from 'jointjs'; 2 | import Supertype from "./supertype"; 3 | import {renderBox, updateBox, removeBox, initializeBox} from '../utils'; 4 | 5 | Supertype.ElementView = dia.ElementView.extend({ 6 | initialize: initializeBox, 7 | render: renderBox, 8 | updateBox: updateBox, 9 | removeBox: removeBox 10 | }); 11 | 12 | export default Supertype; -------------------------------------------------------------------------------- /src/js/schema-diagram/supertype/supertype.js: -------------------------------------------------------------------------------- 1 | import {shapes, util} from 'jointjs'; 2 | import isNil from 'lodash/isNil'; 3 | 4 | const Supertype = shapes.html.Supertype = {}; 5 | 6 | Supertype.Element = shapes.devs.Coupled.extend((function () { 7 | let defaults = util.defaultsDeep({ 8 | type: 'html.Supertype.Element', 9 | attrs: { 10 | '.body': {stroke: '#aaaaaa'}, 11 | }, 12 | }, shapes.devs.Coupled.prototype.defaults); 13 | 14 | /** 15 | * 16 | * @param {DiagramTitle.Element} title 17 | */ 18 | const setDiagramTitle = function (title) { 19 | this.prop('diagramTitle', title); 20 | }; 21 | 22 | const getDiagramTitle = function () { 23 | return this.prop('diagramTitle'); 24 | }; 25 | 26 | /** 27 | * 28 | * @param {DiagramRoot.Element} subtype 29 | */ 30 | const addSubtype = function (subtype) { 31 | if (isNil(this.get('subtypeList'))) this.prop('subtypeList', []); 32 | this.get('subtypeList').push(subtype); 33 | }; 34 | 35 | const getSubtypeList = function () { 36 | if (isNil(this.get('subtypeList'))) this.prop('subtypeList', []); 37 | return this.get('subtypeList'); 38 | }; 39 | 40 | return { 41 | defaults: defaults, 42 | setDiagramTitle: setDiagramTitle, 43 | getDiagramTitle: getDiagramTitle, 44 | addSubtype: addSubtype, 45 | getSubtypeList: getSubtypeList 46 | }; 47 | })()); 48 | 49 | export default Supertype; -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/append-values.js: -------------------------------------------------------------------------------- 1 | import {isNull as _isNull, isUndefined as _isUndefined} from "lodash"; 2 | 3 | const appendValuesToTemplate = function () { 4 | const customAttrs = this.model.get("customAttrs"); 5 | let textValue = ""; 6 | for (let a in customAttrs) { 7 | textValue = (!_isUndefined(customAttrs[a]) && !_isNull(customAttrs[a])) ? customAttrs[a] : ""; 8 | if (!_isUndefined(this.$box) && !_isNull(this.$box)) this.$box.find('div.' + a + '> span').text(textValue); 9 | } 10 | }; 11 | 12 | export default appendValuesToTemplate; -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/index.js: -------------------------------------------------------------------------------- 1 | import renderBox from './render-box'; 2 | import updateBox from "./update-box"; 3 | import removeBox from "./remove-box"; 4 | import initializeBox from "./initialize-box"; 5 | import appendValuesToTemplate from "./append-values"; 6 | 7 | export {renderBox, updateBox, removeBox, initializeBox, appendValuesToTemplate}; -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/initialize-box.js: -------------------------------------------------------------------------------- 1 | import { 2 | bindAll as _bindAll, 3 | isFunction as _isFunction, 4 | isUndefined as _isUndefined, 5 | isNull as _isNull, 6 | template as _template 7 | } from "lodash"; 8 | import {dia} from "jointjs"; 9 | import $ from "jquery"; 10 | 11 | const initializeBox = function () { 12 | _bindAll(this, 'updateBox'); 13 | dia.ElementView.prototype.initialize.apply(this, arguments); 14 | 15 | let rowLevel = this.model.get("rowLevel"); 16 | this.$box = null; 17 | if (!_isUndefined(this.htmlTemplate)) this.$box = $(_template(this.htmlTemplate)({'rowLevel': rowLevel})); 18 | 19 | let flexContainer = null; 20 | if (!_isNull(this.$box)) flexContainer = this.$box.find('.flex-container'); 21 | 22 | if (!_isUndefined(flexContainer) && !_isNull(flexContainer)) { 23 | flexContainer.on('mousedown', (evt) => { evt.stopPropagation(); }); 24 | flexContainer.on('click', (evt) => { evt.stopPropagation(); }); 25 | } 26 | 27 | if (_isFunction(this.appendValuesToTemplate)) this.appendValuesToTemplate(); 28 | if (_isFunction(this.addAdditionalEvents)) this.addAdditionalEvents(); 29 | 30 | this.model.on('change', this.updateBox, this); 31 | this.model.on('remove', this.removeBox, this); 32 | }; 33 | 34 | export default initializeBox; 35 | -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/remove-box.js: -------------------------------------------------------------------------------- 1 | import _isNil from 'lodash/isNil'; 2 | import _has from 'lodash/has'; 3 | const removeBox = function (evt) { 4 | if(_isNil(this)) return; 5 | if(!_has(this, '$box')) return; 6 | if(_isNil(this.$box)) return; 7 | 8 | this.$box.remove(); 9 | }; 10 | 11 | export default removeBox; -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/render-box.js: -------------------------------------------------------------------------------- 1 | import {dia} from "jointjs"; 2 | 3 | const render = function () { 4 | dia.ElementView.prototype.render.apply(this, arguments); 5 | this.listenTo(this.paper, 'scale', this.updateBox); 6 | this.listenTo(this.paper, 'translate', this.updateBox); 7 | this.paper.$el.prepend(this.$box); 8 | this.updateBox(); 9 | return this; 10 | }; 11 | 12 | export default render; -------------------------------------------------------------------------------- /src/js/schema-diagram/utils/update-box.js: -------------------------------------------------------------------------------- 1 | const updateBox = function () { 2 | if (!this.paper) return; 3 | if (!this.$box) return; 4 | if (!this.$box.css) return; 5 | if (!this.getBBox) return; 6 | 7 | const bbox = this.getBBox({ useModelGeometry: true }); 8 | const scale = this.paper.scale(); 9 | 10 | this.$box.css({ 11 | transform: `scale(${scale.sx},${scale.sy})`, 12 | transformOrigin: '0 0', 13 | width: bbox.width / scale.sx, 14 | height: bbox.height / scale.sy, 15 | left: bbox.x, 16 | top: bbox.y, 17 | }); 18 | }; 19 | 20 | export default updateBox; -------------------------------------------------------------------------------- /src/js/ui-script.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | // Toggle the side navigation 4 | $("#sidebarToggle, #sidebarToggleTop").on('click', (e) => { 5 | $("body").toggleClass("sidebar-toggled"); 6 | $(".sidebar").toggleClass("toggled"); 7 | if ($(".sidebar").hasClass("toggled")) { 8 | $('.sidebar .collapse').collapse('hide'); 9 | } 10 | }); 11 | 12 | // Close any open menu accordions when window is resized below 768px 13 | $(window).resize(function () { 14 | if ($(window).width() < 768) { 15 | $('.sidebar .collapse').collapse('hide'); 16 | } 17 | }); 18 | 19 | // Prevent the content wrapper from scrolling when the fixed side navigation hovered over 20 | $('body.fixed-nav .sidebar').on('mousewheel DOMMouseScroll wheel', (e) => { 21 | if ($(window).width() > 768) { 22 | const e0 = e.originalEvent, 23 | delta = e0.wheelDelta || -e0.detail; 24 | this.scrollTop += (delta < 0 ? 1 : -1) * 30; 25 | e.preventDefault(); 26 | } 27 | }); 28 | 29 | // Scroll to top button appear 30 | $(document).on('scroll', () => { 31 | const scrollDistance = $(this).scrollTop(); 32 | if (scrollDistance > 100) { 33 | $('.scroll-to-top').fadeIn(); 34 | } else { 35 | $('.scroll-to-top').fadeOut(); 36 | } 37 | }); 38 | 39 | // Smooth scrolling using jQuery easing 40 | $(document).on('click', 'a.scroll-to-top', (e) => { 41 | const $anchor = $(this); 42 | $('html, body').stop().animate({ 43 | scrollTop: ($($anchor.attr('href')).offset().top) 44 | }, 1000, 'easeInOutExpo'); 45 | e.preventDefault(); 46 | }); 47 | -------------------------------------------------------------------------------- /src/js/visual-schema-editor/html-to-json-schema/index.js: -------------------------------------------------------------------------------- 1 | import isNil from 'lodash/isNil'; 2 | import has from 'lodash/has'; 3 | import isArray from 'lodash/isArray'; 4 | const himalaya = require('himalaya'); 5 | const parse = himalaya.parse; 6 | const parseDefaults = himalaya.parseDefaults; 7 | 8 | function removeEmptyNodes(nodes) { 9 | return nodes.filter(node => { 10 | if (node.type === 'element') { 11 | node.children = removeEmptyNodes(node.children); 12 | return true; 13 | } 14 | return node.content.length; 15 | }); 16 | } 17 | 18 | function stripWhitespace(nodes) { 19 | return nodes.map(node => { 20 | if (node.type === 'element') { 21 | node.children = stripWhitespace(node.children); 22 | } else { 23 | node.content = node.content.trim(); 24 | } 25 | return node; 26 | }); 27 | } 28 | 29 | function removeWhitespace(nodes) { 30 | return removeEmptyNodes(stripWhitespace(nodes)); 31 | } 32 | 33 | /** 34 | * Gets the property name 35 | * 36 | * @param {Object} e a JSON-Object representing a DOM-Element 37 | * @returns the property name 38 | */ 39 | function getPropName(e) { 40 | if (isNil(e)) throw Error("Element is undefined"); 41 | if (!has(e, "attributes")) throw Error("attributes is undefined"); 42 | if (!isArray(e.attributes)) throw Error("attributes isn't an Array"); 43 | 44 | return e.attributes.find(a => a.key === "data-prop-name").value || ""; 45 | } 46 | 47 | /** 48 | * Gets the property type 49 | * 50 | * @param {Object} e a JSON-Object representing a DOM-Element 51 | * @returns the property type 52 | */ 53 | function getPropType(e) { 54 | if (isNil(e)) throw Error("Element is undefined"); 55 | if (!has(e, "attributes")) throw Error("attributes is undefined"); 56 | if (!isArray(e.attributes)) throw Error("attributes isn't an Array"); 57 | 58 | return e.attributes.find(a => a.key === "data-prop-type").value || ""; 59 | } 60 | 61 | /** 62 | * 63 | * @param {Object} e a JSON-Object representing a DOM-Element 64 | * @returns {Object} an object 65 | */ 66 | function extractSimpleRow(e) { 67 | return { 68 | [getPropName(e)]: { 69 | "type": getPropType(e) 70 | } 71 | }; 72 | } 73 | 74 | function extractProperties(e) { 75 | return parseJsonSchema(e.children.find(c => 76 | c.attributes.find(a => 77 | a.value === "new-prop-container")).children.find(c => 78 | c.attributes.find(a => 79 | a.value === "properties")).children.filter(c => 80 | c.type === "element")); 81 | } 82 | 83 | function extractObjectRow(e) { 84 | return { 85 | [getPropName(e)]: { 86 | "type": getPropType(e), 87 | "properties": extractProperties(e) 88 | } 89 | }; 90 | } 91 | 92 | function parseJsonSchema(json) { 93 | return Object.assign({}, ...json.map(node => { 94 | const propType = getPropType(node); 95 | if (["string", "number", "integer", "boolean"].indexOf(propType) > -1) return extractSimpleRow(node); 96 | if (["object", "array"].indexOf(propType) > -1) return extractObjectRow(node); 97 | })); 98 | } 99 | 100 | /* 101 | $('#parser-btn').on('click', function () { 102 | let result = parse( 103 | $('#schema-editor > .object-row > .new-prop-container > .properties').html(), 104 | Object.assign({includePositions: true}, parseDefaults) 105 | ); 106 | result = removeWhitespace(result).filter(function (i) { 107 | return i.type === "element"; 108 | }); 109 | console.log({properties: parseJsonSchema(result)}); 110 | }); 111 | */ 112 | 113 | export const htmlToJsonSchema = function (properties) { 114 | let result = parse(properties, Object.assign({includePositions: true}, parseDefaults)); 115 | result = removeWhitespace(result).filter(function (i) { return i.type === "element";}); 116 | result = {properties: parseJsonSchema(result)}; 117 | return result; 118 | }; -------------------------------------------------------------------------------- /src/js/visual-schema-editor/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import {htmlToJsonSchema} from "./html-to-json-schema"; 3 | import {createSeObjectRow} from "./object-row"; 4 | import {createDiagramRoot} from "../jointjs-helper/diagram-generator"; 5 | 6 | const schemaEditorButton = $('#schema-editor-btn'); 7 | const schemaEditorModal = $('#schema-editor-modal'); 8 | const visualizeButton = schemaEditorModal.find('.visualize-btn'); 9 | const schemaTitleInput = schemaEditorModal.find('#schema-title-input'); 10 | const actionButton = schemaEditorModal.find('.visualize-btn'); 11 | 12 | schemaEditorButton.on('click', function () { 13 | schemaEditorModal.modal('show'); 14 | }); 15 | 16 | visualizeButton.on('click', function () { 17 | const properties = $('#schema-editor').children('.object-row').children('.new-prop-container').children('.properties'); 18 | if(properties.children().length < 1) { 19 | alert('At least one property is required'); 20 | return; 21 | } 22 | const jsonSchema = htmlToJsonSchema(properties.html()); 23 | jsonSchema["title"] = schemaTitleInput.val() || "Entity_type"; 24 | createDiagramRoot(jsonSchema); 25 | schemaEditorModal.modal('hide'); 26 | }); 27 | 28 | schemaEditorModal.on('show.bs.modal', function () { 29 | schemaTitleInput.val(''); 30 | schemaTitleInput.removeClass('is-valid').addClass('is-invalid'); 31 | 32 | const row = createSeObjectRow('ROOT', 'object'); 33 | const schemaEditor = $('#schema-editor'); 34 | schemaEditor.empty(); 35 | schemaEditor.append(row); 36 | 37 | $('#schema-editor > .object-row > .new-prop-container > .new-field-elements > .add-btn').prop("disabled", true); 38 | $('#schema-editor > .object-row > .form-inline > .remove-btn-block > .remove-btn').first().remove(); 39 | $('#schema-editor > .object-row > .simple-row > .property-info > .field-name > .expander-btn').remove(); 40 | }); 41 | 42 | schemaTitleInput.on('input propertychange', function(){ 43 | const invalidInputFeedback = schemaEditorModal.find('.invalid-feedback'); 44 | if(isFieldNameValid(this.value)){ 45 | actionButton.prop("disabled", false); 46 | schemaTitleInput.removeClass('is-invalid').addClass('is-valid'); 47 | invalidInputFeedback.fadeOut(); 48 | } else { 49 | actionButton.prop("disabled", true); 50 | schemaTitleInput.removeClass('is-valid').addClass('is-invalid'); 51 | invalidInputFeedback.fadeIn(); 52 | } 53 | }); 54 | 55 | function isFieldNameValid(value){ 56 | return (/^([a-zA-Z0-9_]+)$/.test(value)); 57 | } 58 | -------------------------------------------------------------------------------- /src/js/visual-schema-editor/object-row/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import uuidV4 from 'uuid/v4'; 3 | import _template from 'lodash/template'; 4 | import {createSeSimpleRow} from '../simple-row'; 5 | import objectRowTemplate from './template.html'; 6 | 7 | function createIDs() { 8 | return { 9 | newPropContainerId: `new-prop-container-${uuidV4()}`, 10 | propertiesId: `properties-${uuidV4()}`, 11 | newPropBlockId: `new-prop-block-${uuidV4()}`, 12 | newFieldElementsId: `new-field-elements-${uuidV4()}`, 13 | fieldNameInputId: `field-name-input-${uuidV4()}`, 14 | fieldTypeSelectionId: `field-type-selection-${uuidV4()}`, 15 | addButtonId: `add-button-${uuidV4()}`, 16 | cancelButtonId: `cancel-button-${uuidV4()}`, 17 | removeButtonId: `remove-button-${uuidV4()}`, 18 | expanderButtonId: `expander-button-${uuidV4()}`, 19 | }; 20 | } 21 | 22 | function findElements(row, IDs) { 23 | return { 24 | properties: row.find(`#${IDs.propertiesId}`), 25 | newPropContainer: row.find(`#${IDs.newPropContainerId}`), 26 | newPropBlock: row.find(`#${IDs.newPropBlockId}`), 27 | newFieldElements: row.find(`#${IDs.newFieldElementsId}`), 28 | fieldNameInput: row.find(`#${IDs.fieldNameInputId}`), 29 | fieldTypeSelection: row.find(`#${IDs.fieldTypeSelectionId}`), 30 | addButton: row.find(`#${IDs.addButtonId}`), 31 | cancelButton: row.find(`#${IDs.cancelButtonId}`), 32 | removeButton: row.find(`#${IDs.removeButtonId}`), 33 | expanderButton: row.find(`#${IDs.expanderButtonId}`), 34 | }; 35 | } 36 | 37 | function isFieldNameValid(value) { 38 | return (/^([a-zA-Z0-9_]+)$/.test(value)); 39 | } 40 | 41 | export const createSeObjectRow = function createSeObjectRow(field_Name, field_Type) { 42 | const IDs = createIDs(); 43 | 44 | const row = $(_template(objectRowTemplate)({ 45 | fieldName: field_Name, 46 | fieldType: field_Type, 47 | IDs: IDs, 48 | })); 49 | 50 | const { 51 | properties, newPropContainer, newPropBlock, newFieldElements, fieldNameInput, fieldTypeSelection, 52 | addButton, cancelButton, removeButton, expanderButton 53 | } = findElements(row, IDs); 54 | 55 | expanderButton.on('click', function () { 56 | if (properties.children().length < 1) return; 57 | 58 | let isExpanded = ($(this).attr('data-expanded') === 'true'); 59 | 60 | if (isExpanded) { 61 | $(this).attr('data-expanded', 'false'); 62 | $(this).children('i').toggleClass('fa-chevron-down fa-chevron-right'); 63 | 64 | newPropContainer.slideUp(); 65 | } else { 66 | $(this).attr('data-expanded', 'true'); 67 | $(this).children('i').toggleClass('fa-chevron-right fa-chevron-down'); 68 | 69 | newPropContainer.slideDown(); 70 | } 71 | }); 72 | 73 | fieldNameInput.on('input propertychange', function () { 74 | if (isFieldNameValid(this.value)) { 75 | newFieldElements.children('.add-btn').prop("disabled", false); 76 | } else { 77 | newFieldElements.children('.add-btn').prop("disabled", true); 78 | } 79 | }); 80 | 81 | 82 | newPropBlock.children('button').on('click', function () { 83 | newPropBlock.css('display', 'none'); 84 | newFieldElements.fadeIn('500'); 85 | }); 86 | 87 | cancelButton.on('click', function () { 88 | newFieldElements.css('display', 'none'); 89 | newPropBlock.fadeIn('500'); 90 | resetInput(); 91 | }); 92 | 93 | addButton.on('click', function () { 94 | const fieldName = fieldNameInput.val(); 95 | 96 | if (_.isEmpty(fieldName)) { 97 | alert('Property name must be a valid string'); 98 | return; 99 | } 100 | 101 | const fieldType = fieldTypeSelection.children("option:selected").val(); 102 | row.find('.empty-placeholder').remove(); 103 | 104 | switch (fieldType) { 105 | case 'string': 106 | case 'integer': 107 | case 'number': 108 | case 'boolean': 109 | const simpleRow = createSeSimpleRow(fieldName, fieldType).hide(); 110 | properties.append(simpleRow); 111 | simpleRow.fadeIn(); 112 | break; 113 | 114 | case 'object': 115 | case 'array': 116 | const objectRow = createSeObjectRow(fieldName, fieldType).hide(); 117 | properties.append(objectRow); 118 | objectRow.fadeIn(); 119 | break; 120 | } 121 | 122 | newFieldElements.css('display', 'none'); 123 | newPropBlock.fadeIn('500'); 124 | resetInput(); 125 | }); 126 | 127 | removeButton.on('click', function () { 128 | row.fadeOut('100', function () { 129 | row.remove(); 130 | }); 131 | }); 132 | 133 | function resetInput() { 134 | fieldNameInput.val(''); 135 | fieldTypeSelection.val('string'); 136 | addButton.prop("disabled", true); 137 | } 138 | 139 | return row; 140 | }; -------------------------------------------------------------------------------- /src/js/visual-schema-editor/object-row/template.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 9 | <%= fieldName %> 10 | 11 | <%= fieldType %> 12 |
    13 |
    14 | 17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 | 24 |
    25 | 28 |
    29 | 49 |
    50 |
    -------------------------------------------------------------------------------- /src/js/visual-schema-editor/simple-row/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import uuidV4 from 'uuid/v4'; 3 | import _template from 'lodash/template'; 4 | import simpleRowTemplate from './template.html'; 5 | 6 | function isFieldNameValid(value) { 7 | return (/^([a-zA-Z0-9_]+)$/.test(value)); 8 | } 9 | 10 | function createIDs() { 11 | return { 12 | propertyInfoId: `property-info-${uuidV4()}`, 13 | propertyEditorId: `property-editor-${uuidV4()}`, 14 | nameInputId: `field-name-input-${uuidV4()}`, 15 | typeSelectionId: `field-type-selection-${uuidV4()}`, 16 | removeButtonId: `remove-button-${uuidV4()}`, 17 | addButtonId: `add-button-${uuidV4()}`, 18 | cancelButtonId: `cancel-button-${uuidV4()}`, 19 | editButtonId: `edit-button-${uuidV4()}`, 20 | }; 21 | } 22 | 23 | function findElements(row, IDs) { 24 | return { 25 | propertyInfo: row.find(`#${IDs.propertyInfoId}`), 26 | propertyEditor: row.find(`#${IDs.propertyEditorId}`), 27 | nameInput: row.find(`#${IDs.nameInputId}`), 28 | typeSelection: row.find(`#${IDs.typeSelectionId}`), 29 | removeButton: row.find(`#${IDs.removeButtonId}`), 30 | addButton: row.find(`#${IDs.addButtonId}`), 31 | cancelButton: row.find(`#${IDs.cancelButtonId}`), 32 | editButton: row.find(`#${IDs.editButtonId}`), 33 | }; 34 | } 35 | 36 | export const createSeSimpleRow = function (fieldName, fieldType) { 37 | const IDs = createIDs(); 38 | 39 | const row = $(_template(simpleRowTemplate)({ 40 | fieldName: fieldName, 41 | fieldType: fieldType, 42 | IDs: IDs 43 | })); 44 | 45 | const { 46 | propertyInfo, propertyEditor, nameInput, typeSelection, removeButton, addButton, cancelButton, editButton 47 | } = findElements(row, IDs); 48 | 49 | removeButton.on('click', function () { 50 | row.fadeOut('100', function () { 51 | row.remove(); 52 | }); 53 | }); 54 | 55 | editButton.on('click', function () { 56 | nameInput.val(row.attr('data-prop-name')); 57 | typeSelection.val(row.attr('data-prop-type')); 58 | addButton.prop("disabled", false); 59 | 60 | propertyInfo.css('display', 'none'); 61 | propertyEditor.fadeIn('500'); 62 | }); 63 | 64 | nameInput.on('input propertychange', function () { 65 | if (isFieldNameValid(this.value)) { 66 | addButton.prop("disabled", false); 67 | } else { 68 | addButton.prop("disabled", true); 69 | } 70 | }); 71 | 72 | addButton.on('click', function () { 73 | propertyInfo.children('.field-name').text(nameInput.val()); 74 | propertyInfo.children('.field-type').text(typeSelection.children("option:selected").val()); 75 | 76 | row.attr('data-prop-name', nameInput.val()); 77 | row.attr('data-prop-type', typeSelection.children("option:selected").val()); 78 | 79 | propertyEditor.css('display', 'none'); 80 | propertyInfo.fadeIn('500'); 81 | }); 82 | 83 | cancelButton.on('click', function () { 84 | propertyEditor.css('display', 'none'); 85 | propertyInfo.fadeIn('500'); 86 | }); 87 | 88 | return row; 89 | }; 90 | -------------------------------------------------------------------------------- /src/js/visual-schema-editor/simple-row/template.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%= fieldName %> 4 | <%= fieldType %> 5 |
    6 | 24 |
    25 | 28 | 31 |
    32 |
    --------------------------------------------------------------------------------