├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── stale.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── e2e ├── jest-puppeteer.config.js ├── jest.config.js ├── pageObjects │ └── index.js └── specs │ └── index.js ├── package.json ├── public ├── _redirects ├── android-chrome-96x96.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── manifest.json ├── mstile-150x150.png ├── robots.txt └── safari-pinned-tab.svg ├── src ├── __tests__ │ └── utils │ │ └── upperFirst.test.js ├── assets │ └── images │ │ ├── Article │ │ ├── Article 1.svg │ │ ├── Article 2.svg │ │ ├── Article 3.svg │ │ ├── Article 4.svg │ │ ├── Article 5.svg │ │ └── Article 6.svg │ │ ├── Blog │ │ ├── Articles 1.svg │ │ ├── Articles 10.svg │ │ ├── Articles 11.svg │ │ ├── Articles 12.svg │ │ ├── Articles 2.svg │ │ ├── Articles 3.svg │ │ ├── Articles 4.svg │ │ ├── Articles 5.svg │ │ ├── Articles 6.svg │ │ ├── Articles 7.svg │ │ ├── Articles 8.svg │ │ └── Articles 9.svg │ │ ├── E-Commerce │ │ ├── Cart Popup.svg │ │ ├── Cart.svg │ │ ├── Checkout.svg │ │ ├── Complete.svg │ │ ├── Delivery.svg │ │ ├── Item 2.svg │ │ ├── Item.svg │ │ ├── Paypal.svg │ │ ├── Products 1.svg │ │ ├── Products 2.svg │ │ ├── Products 3.svg │ │ └── Rate.svg │ │ ├── Features │ │ ├── Features 1.svg │ │ ├── Features 2.svg │ │ ├── Features 3.svg │ │ ├── Features 4.svg │ │ ├── Features 5.svg │ │ └── Features 6.svg │ │ ├── Gallery │ │ ├── Gallery 1.svg │ │ ├── Gallery 2.svg │ │ ├── Gallery 3.svg │ │ ├── Gallery 4.svg │ │ ├── Gallery 5.svg │ │ └── Gallery 6.svg │ │ ├── Header │ │ ├── Header 1.svg │ │ ├── Header 2.svg │ │ ├── Header 3.svg │ │ ├── Header 4.svg │ │ ├── Header 5.svg │ │ └── Header 6.svg │ │ ├── Misc │ │ ├── 404.svg │ │ ├── About.svg │ │ ├── Analitycs.svg │ │ ├── Calender.svg │ │ ├── Cards.svg │ │ ├── Contact.svg │ │ ├── Counter.svg │ │ ├── Error.svg │ │ ├── Faqs.svg │ │ ├── Forum.svg │ │ ├── Loading.svg │ │ ├── Price 1.svg │ │ ├── Price 2.svg │ │ ├── Progress.svg │ │ ├── Search results.svg │ │ ├── Search.svg │ │ ├── Settings.svg │ │ ├── Sitemap.svg │ │ ├── Socials.svg │ │ ├── Steps.svg │ │ ├── Subscribde.svg │ │ ├── Tags.svg │ │ ├── Team.svg │ │ └── Under Contruction.svg │ │ ├── Multimedia │ │ ├── Files.svg │ │ ├── Songs 1.svg │ │ ├── Songs 2.svg │ │ ├── Songs 3.svg │ │ ├── Upload Files.svg │ │ ├── Upload Image.svg │ │ ├── Video Player 1.svg │ │ ├── Video Player 2.svg │ │ ├── Videos 1.svg │ │ ├── Videos 2.svg │ │ ├── Videos 3.svg │ │ └── Videos 4.svg │ │ ├── Sign in │ │ ├── Forgot Password 1.svg │ │ ├── Forgot Password 2.svg │ │ ├── Sign Up 1.svg │ │ ├── Sign Up 2.svg │ │ ├── Sign in 1.svg │ │ └── Sign in 2.svg │ │ └── Socials │ │ ├── Chat.svg │ │ ├── Comments.svg │ │ ├── Connection.svg │ │ ├── Feeds.svg │ │ ├── Profile 1.svg │ │ ├── Profile 2.svg │ │ ├── Profile 3.svg │ │ ├── Profile 4.svg │ │ ├── User Settings 2.svg │ │ ├── User Settings.svg │ │ ├── Users 2.svg │ │ └── Users.svg ├── components │ ├── ExportCanvas │ │ ├── index.js │ │ └── style.css │ ├── FlowCanvas │ │ ├── index.js │ │ └── style.css │ ├── FlowDetailPanel │ │ ├── DetailForm │ │ │ └── index.js │ │ ├── index.js │ │ └── style.css │ ├── FlowItemPanel │ │ ├── NodeItem.js │ │ ├── index.js │ │ ├── nodesData.js │ │ └── style.css │ ├── FlowMiniMap │ │ └── index.js │ ├── FlowToolbar │ │ ├── ToolbarButton.js │ │ ├── index.js │ │ └── style.css │ └── IconFont │ │ └── index.js ├── containers │ ├── App │ │ └── index.js │ └── register │ │ └── node │ │ └── index.js ├── index.css ├── index.js ├── serviceWorker.js ├── setupTests.js └── utils │ ├── dataMapToData.js │ ├── index.js │ └── saveData.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | *e2e 2 | *.git* 3 | *.editorconfig 4 | *.eslintignore 5 | *.eslintrc.js 6 | *build -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | imports/lib/*.min.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "meteor": true, 7 | "mocha": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": true 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "react", 19 | "mocha" 20 | ], 21 | "rules": { 22 | "indent": [ 23 | "warn", 24 | 2 25 | ], 26 | "linebreak-style": [ 27 | "warn", 28 | "unix" 29 | ], 30 | "quotes": [ 31 | "warn", 32 | "single" 33 | ], 34 | "semi": [ 35 | "error", 36 | "always" 37 | ], 38 | "mocha/no-exclusive-tests": "error", 39 | "react/jsx-uses-react": "error", 40 | "react/jsx-uses-vars": "error", 41 | "no-console": "warn", 42 | "no-unused-vars": "warn" 43 | } 44 | }; -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | 7 | # Issues with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | 21 | # Comment to post when closing a stale issue. Set to `false` to disable 22 | closeComment: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | build -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | WORKDIR /app 3 | COPY . . 4 | RUN yarn 5 | RUN yarn build 6 | 7 | FROM nginx 8 | WORKDIR /usr/share/nginx/html/ 9 | COPY --from=0 /app/build/ . 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2019 The Vanila Team 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 | # Wireflow - flow chart collaboration app 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/15abd946-68e7-4cdb-8d9e-4930d5a2191c/deploy-status)](https://app.netlify.com/sites/wireflow-app/deploys) 4 | [![dependencies Status](https://david-dm.org/vanila-io/wireflow/status.svg)](https://david-dm.org/vanila-io/wireflow) 5 | [![devDependencies Status](https://david-dm.org/vanila-io/wireflow/dev-status.svg)](https://david-dm.org/vanila-io/wireflow?type=dev) 6 | [![OpenCollective](https://opencollective.com/wireflow/backers/badge.svg)](#backers) 7 | [![OpenCollective](https://opencollective.com/wireflow/sponsors/badge.svg)](#sponsors) 8 | 9 | Alpha version of Wireflow app made by [The Vanila Team](https://vanila.io) and [Automatio AI](https://automatio.ai). 10 | 11 | ### Official Website: [Wireflow.co](https://wireflow.co) 12 | 13 | ![Wireflow](https://i.imgur.com/ceXMd28.png) 14 | 15 | ## Around the web: 16 | 17 | - [Private slack channel invite link](https://join.slack.com/t/wireflow/shared_invite/zt-iwgx8efa-Vt~_rnkw2tGAhSR~nJs9bA) 18 | - Join our community chat: https://community.vanila.io/wireflow 19 | - [Youtube Video with a short story](https://youtu.be/zm0XbLmXtXY) 20 | - [Post regarding Contribution](https://forums.meteor.com/t/anyone-interested-in-collaboration-on-wireflow-co-open-source-project/40716) 21 | - [Check a blog post for whole story](https://blog.vanila.io/we-were-hunted-on-producthunt-unexpectedly-e92e7179bdec) 22 | - [ProductHunt page](https://www.producthunt.com/posts/wireflow) 23 | - [Open Hub analysis of Wireflow code](https://www.openhub.net/p/wireflow) 24 | 25 | # Develop Locally 26 | 27 | ``` 28 | yarn 29 | yarn start 30 | open http://localhost:3000 31 | ``` 32 | 33 | # Using docker-compose 34 | ``` 35 | docker-compose up -d 36 | ``` 37 | 38 | ## Credits 39 | ### Contributors 40 | 41 | This project exists thanks to all the people who contribute. 42 | 43 | 44 | 45 | ### Backers 46 | 47 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/wireflow#backer)] 48 | 49 | 50 | 51 | 52 | ### Sponsors 53 | 54 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/wireflow#sponsor)] 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | web: 5 | image: debarshri/wireflow 6 | build: . 7 | ports: 8 | - 8083:80 9 | environment: 10 | - MONGO_URL="mongodb://localhost:27018 meteor --port 3050" 11 | db: 12 | image: mongo:latest 13 | ports: 14 | - 3050:3050 -------------------------------------------------------------------------------- /e2e/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | launch: { 3 | headless: false, 4 | slowMo: 300, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | globals: { 4 | URL: 'http://localhost:3000', 5 | }, 6 | testMatch: ['**/specs/*.js'], 7 | transform: { 8 | '\\.js$': 'react-scripts/config/jest/babelTransform', 9 | }, 10 | verbose: true, 11 | }; 12 | -------------------------------------------------------------------------------- /e2e/pageObjects/index.js: -------------------------------------------------------------------------------- 1 | const mouse = page.mouse; 2 | 3 | export const clientXY = async (selector) => 4 | await page.$eval(selector, (e) => [e.offsetLeft, e.offsetTop]); 5 | 6 | export const moveDown = async (x, y) => { 7 | await mouse.move(x, y); 8 | await mouse.down(); 9 | }; 10 | 11 | export const moveUp = async (x, y) => { 12 | await mouse.move(x, y); 13 | await mouse.up(); 14 | }; 15 | -------------------------------------------------------------------------------- /e2e/specs/index.js: -------------------------------------------------------------------------------- 1 | import { clientXY, moveDown, moveUp } from '../pageObjects'; 2 | 3 | const NODE_SELECT_ONE = 'div.sidebar div:nth-child(1)'; 4 | const NODE_SELECT_TWO = 'div.sidebar div:nth-child(2)'; 5 | 6 | const NODE_DELETE_BUTTON = 'div.toolbar'; 7 | 8 | describe('Wireflow', () => { 9 | beforeAll(async () => { 10 | await page.goto(URL); 11 | await page.setViewport({ width: 1280, height: 800 }); 12 | }); 13 | 14 | const navigationPromise = page.waitForNavigation(); 15 | 16 | it('should be add node (1) on canvas', async (done) => { 17 | const [, clientY] = await clientXY(NODE_SELECT_ONE); 18 | await moveDown(70, clientY + 30); 19 | await moveUp(200, 200); 20 | await navigationPromise; 21 | done(); 22 | }); 23 | 24 | it('should be add title on node (1)', async (done) => { 25 | await moveDown(210, 210); 26 | await page.mouse.up(); 27 | await page.waitForSelector('.ant-form'); 28 | await page.click('input[name=title]'); 29 | await navigationPromise; 30 | await page.type('input[name=title]', 'Node title'); 31 | await moveDown(500, 0); 32 | await page.mouse.up(); 33 | await navigationPromise; 34 | done(); 35 | }, 1600000); 36 | 37 | it('should be add node (2) on canvas', async (done) => { 38 | const [, clientY] = await clientXY(NODE_SELECT_TWO); 39 | await moveDown(70, clientY + 30); 40 | await moveUp(400, 400); 41 | await navigationPromise; 42 | done(); 43 | }); 44 | 45 | it('should be add edge between two nodes', async (done) => { 46 | await moveDown(210, 210); 47 | await page.mouse.up(); 48 | await navigationPromise; 49 | await moveDown(200, 250); 50 | await page.mouse.move(300, 200); 51 | await moveUp(400, 450); 52 | await navigationPromise; 53 | done(); 54 | }); 55 | 56 | it('should be update edge size', async (done) => { 57 | await moveDown(200, 270); 58 | await page.mouse.up(); 59 | const [clientX] = await clientXY('div.ant-col-5'); 60 | await moveDown(clientX + 100, 180); 61 | await moveUp(clientX + 130, 180); 62 | await navigationPromise; 63 | await moveDown(500, 0); 64 | await page.mouse.up(); 65 | await navigationPromise; 66 | done(); 67 | }); 68 | 69 | it('should be delete edge from canvas', async (done) => { 70 | const [clientX, clientY] = await clientXY(NODE_DELETE_BUTTON); 71 | await moveDown(200, 270); 72 | await page.mouse.up(); 73 | await page.mouse.click(clientX + 10, clientY + 15); 74 | await navigationPromise; 75 | done(); 76 | }); 77 | 78 | it('should be delete node (1) from canvas', async () => { 79 | const [x, y] = await clientXY(NODE_DELETE_BUTTON); 80 | await moveDown(410, 410); 81 | await page.mouse.up(); 82 | await page.mouse.click(x + 10, y + 15); 83 | await page.waitFor(1000); 84 | await navigationPromise; 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wireflow", 3 | "version": "0.0.4", 4 | "private": true, 5 | "description": "Flowchart collaboration App", 6 | "scripts": { 7 | "build": "CI= react-scripts build", 8 | "e2e": "cd e2e && jest", 9 | "eject": "react-scripts eject", 10 | "start": "react-scripts start", 11 | "test": "react-scripts test --watchAll=false" 12 | }, 13 | "browserslist": { 14 | "production": [ 15 | ">0.2%", 16 | "not dead", 17 | "not op_mini all" 18 | ], 19 | "development": [ 20 | "last 1 chrome version", 21 | "last 1 firefox version", 22 | "last 1 safari version" 23 | ] 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "dependencies": { 29 | "@ant-design/icons": "^4.0.6", 30 | "antd": "^4.1.4", 31 | "gg-editor": "^2.0.3", 32 | "html-to-image": "^0.1.1", 33 | "react": "^16.13.1", 34 | "react-colorful": "^4.0.4", 35 | "react-dom": "^16.13.1", 36 | "react-scripts": "3.4.1" 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^5.9.0", 40 | "@testing-library/react": "^10.0.6", 41 | "@testing-library/user-event": "^12.0.11", 42 | "@types/expect-puppeteer": "^4.4.3", 43 | "@types/jest-environment-puppeteer": "^4.3.1", 44 | "@types/puppeteer": "^3.0.0", 45 | "jest-puppeteer": "^4.4.0", 46 | "puppeteer": "^5.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/android-chrome-96x96.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 31 | Wireflow 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Wireflow", 3 | "name": "Wireflow", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "mstile-150x150.png", 12 | "type": "image/png", 13 | "sizes": "150x150" 14 | }, 15 | { 16 | "src": "android-chrome-96x96.png", 17 | "type": "image/png", 18 | "sizes": "96x96" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#ffffff", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vanila-io/wireflow/696c838364907cf1cf4420ed0e05a432e5f816d9/public/mstile-150x150.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 16 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/__tests__/utils/upperFirst.test.js: -------------------------------------------------------------------------------- 1 | import { upperFirst } from "../../utils"; 2 | 3 | describe("upperFirst", () => { 4 | it("should return one word with only first letter capitalised when one word is passed in", () => { 5 | const result = upperFirst("wIREfloW"); 6 | expect(result).toEqual("Wireflow"); 7 | }); 8 | 9 | it("should return each word having only first letter capitalised when multiple words are passed in", () => { 10 | const result = upperFirst("hello, wIREfloW PEOPLE"); 11 | expect(result).toEqual("Hello, Wireflow People"); 12 | }); 13 | }) -------------------------------------------------------------------------------- /src/assets/images/Article/Article 6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 13 | 16 | 20 | 25 | 35 | 37 | 40 | 43 | 45 | 47 | 49 | 51 | 54 | 57 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/assets/images/E-Commerce/Paypal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 20 | 25 | 37 | 48 | 55 | 59 | 63 | 67 | 69 | 71 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/assets/images/E-Commerce/Products 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 27 | 28 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 44 | 47 | 49 | 51 | 53 | 55 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/assets/images/Features/Features 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 28 | 31 | 33 | 35 | 37 | 40 | 43 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/assets/images/Misc/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 33 | 37 | 40 | 44 | 46 | 48 | 53 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Analitycs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 41 | 44 | 46 | 48 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Counter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 49 | 51 | 52 | 53 | 55 | 58 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 30 | 35 | 41 | 45 | 48 | 52 | 54 | 56 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Faqs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 28 | 30 | 32 | 35 | 38 | 40 | 42 | 44 | 46 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Forum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 26 | 27 | 29 | 31 | 33 | 35 | 36 | 37 | 39 | 41 | 42 | 43 | 45 | 47 | 48 | 49 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 29 | 32 | 35 | 38 | 41 | 44 | 47 | 50 | 52 | 54 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Progress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 24 | 29 | 33 | 36 | 40 | 43 | 47 | 49 | 51 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 21 | 25 | 30 | 32 | 34 | 37 | 41 | 44 | 47 | 49 | 51 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Sitemap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Steps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 38 | 40 | 42 | 44 | 47 | 49 | 51 | 54 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Tags.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 29 | 31 | 35 | 37 | 39 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/assets/images/Misc/Under Contruction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 27 | 46 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/assets/images/Multimedia/Songs 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 28 | 30 | 35 | 38 | 39 | 41 | 43 | 45 | 48 | 49 | 51 | 54 | 55 | 57 | 59 | 60 | 62 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/assets/images/Multimedia/Songs 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 28 | 30 | 33 | 35 | 37 | 39 | 41 | 46 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/assets/images/Sign in/Sign Up 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 25 | 28 | 29 | 31 | 32 | 35 | 36 | 43 | 47 | 50 | 54 | 57 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/assets/images/Sign in/Sign in 1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 25 | 28 | 29 | 31 | 32 | 35 | 36 | 43 | 47 | 50 | 54 | 57 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/assets/images/Socials/Profile 2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 17 | 21 | 26 | 29 | 31 | 34 | 36 | 39 | 42 | 45 | 47 | 49 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/assets/images/Socials/Profile 3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 13 | 16 | 20 | 25 | 28 | 30 | 32 | 34 | 37 | 44 | 47 | 50 | 54 | 64 | 66 | 68 | 71 | 73 | 75 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/components/ExportCanvas/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from 'antd/es/button'; 3 | import 'antd/es/button/style/css'; 4 | import htmlToImage from 'html-to-image'; 5 | import { ContextMenu, Command, CanvasMenu } from 'gg-editor'; 6 | 7 | import IconFont from '../IconFont'; 8 | import './style.css'; 9 | 10 | const ExportCanvas = () => { 11 | function saveCanvas() { 12 | htmlToImage 13 | .toJpeg(document.getElementById('canvas_1'), { quality: 1 }) 14 | .then(function (dataUrl) { 15 | var link = document.createElement('a'); 16 | link.download = 'wireflow.jpg'; 17 | link.href = dataUrl; 18 | link.click(); 19 | }); 20 | } 21 | 22 | return ( 23 | 24 | 25 | 26 |
27 |
35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default ExportCanvas; 42 | -------------------------------------------------------------------------------- /src/components/ExportCanvas/style.css: -------------------------------------------------------------------------------- 1 | .export { 2 | position: absolute; 3 | left: 20px; 4 | top: 20px; 5 | z-index: 999; 6 | } 7 | #canvas_1 { 8 | background: #f0f2f5; 9 | } 10 | #J_ContextMenuContainer_2 { 11 | display: block !important; 12 | } 13 | #J_ContextMenuContainer_2 .menu { 14 | display: block !important; 15 | } 16 | 17 | #J_ContextMenuContainer_1 { 18 | display: block !important; 19 | } 20 | #J_ContextMenuContainer_1 .menu { 21 | display: block !important; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/FlowCanvas/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Flow } from 'gg-editor'; 3 | import './style.css'; 4 | import { dataMapToData } from '../../utils/dataMapToData'; 5 | import { saveData } from '../../utils/saveData'; 6 | 7 | const data = JSON.parse(localStorage.getItem('data')); 8 | 9 | const FlowCanvas = () => { 10 | const [edge, setEdge] = useState({}); 11 | const [oncanvas, setOnCanvas] = useState(false); 12 | 13 | const mouseEvent = async (e) => { 14 | const event = await e; 15 | const EVENT_TYPE = e._type; 16 | 17 | if (!event?.item) { 18 | switch (EVENT_TYPE) { 19 | case 'mouseleave': 20 | setOnCanvas(true); 21 | break; 22 | case 'mouseenter': 23 | setOnCanvas(false); 24 | break; 25 | default: 26 | break; 27 | } 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | if (edge.type === 'edge') { 33 | oncanvas ? (edge.isSelected = false) : (edge.isSelected = true); 34 | } 35 | }, [oncanvas, edge]); 36 | 37 | return ( 38 | { 40 | const item = await e.item; 41 | 42 | setEdge(item); 43 | }} 44 | onAfterChange={(e) => { 45 | // `changeData` is caused by setData and allowing `group` causes some error 46 | if ( 47 | e.action === 'changeData' || 48 | (e.item.type === 'group' && e.action !== 'remove') 49 | ) { 50 | return; 51 | } 52 | 53 | saveData(dataMapToData(e.item && e.item.dataMap, e.item.itemMap)); 54 | }} 55 | data={data} 56 | onBeforeItemUnselected={() => setEdge({})} 57 | onMouseEnter={mouseEvent} 58 | onMouseLeave={mouseEvent} 59 | className='flow' 60 | /> 61 | ); 62 | }; 63 | 64 | export default FlowCanvas; 65 | -------------------------------------------------------------------------------- /src/components/FlowCanvas/style.css: -------------------------------------------------------------------------------- 1 | .flow { 2 | height: 100vh; 3 | overflow: hidden; 4 | } 5 | /* .flow #canvas_1 { 6 | background: #ddf; 7 | } */ 8 | -------------------------------------------------------------------------------- /src/components/FlowDetailPanel/DetailForm/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withPropsAPI } from 'gg-editor'; 4 | import Card from 'antd/es/card'; 5 | import Input from 'antd/es/input'; 6 | import Select from 'antd/es/select'; 7 | import Form from 'antd/es/form'; 8 | import Slider from 'antd/es/slider'; 9 | import Descriptions from 'antd/es/descriptions'; 10 | import { HexColorPicker as ColorPicker } from 'react-colorful'; 11 | 12 | import 'antd/es/card/style/css'; 13 | import 'antd/es/input/style/css'; 14 | import 'antd/es/select/style/css'; 15 | import 'antd/es/form/style/css'; 16 | import 'antd/es/slider/style/css'; 17 | import 'react-colorful/dist/index.css'; 18 | import 'antd/es/descriptions/style/css'; 19 | 20 | import { upperFirst } from '../../../utils'; 21 | 22 | const { Item } = Form; 23 | 24 | const inlineFormItemLayout = { 25 | labelCol: { 26 | sm: { span: 6 }, 27 | }, 28 | wrapperCol: { 29 | sm: { span: 18 }, 30 | }, 31 | }; 32 | 33 | class DetailForm extends React.Component { 34 | get item() { 35 | const { propsAPI } = this.props; 36 | return propsAPI.getSelected()[0]; 37 | } 38 | 39 | handleFieldChange = (values) => { 40 | const { 41 | propsAPI: { getSelected, executeCommand, update }, 42 | } = this.props; 43 | 44 | const item = getSelected()[0]; 45 | if (!item) return; 46 | 47 | executeCommand(() => update(item, { ...values })); 48 | }; 49 | 50 | handleInputBlur = (type) => (e) => { 51 | e.preventDefault(); 52 | 53 | this.handleFieldChange({ 54 | [type]: e.currentTarget.value, 55 | }); 56 | }; 57 | 58 | renderNodeDetail = () => { 59 | const { label } = this.item.getModel(); 60 | 61 | document.addEventListener( 62 | 'keydown', 63 | (e) => { 64 | const { ctrlKey, key } = e; 65 | 66 | if (ctrlKey && key === 'h') { 67 | this.handleFieldChange({ 68 | shape: 'node-image-without-header', 69 | size: [96, 78], 70 | }); 71 | } 72 | 73 | if (ctrlKey && key === 'k') { 74 | this.handleFieldChange({ 75 | shape: 'node-image-header', 76 | size: [96, 88], 77 | }); 78 | } 79 | }, 80 | true 81 | ); 82 | 83 | return ( 84 | <> 85 |
86 | 87 | 88 | 89 |
90 | 96 | 97 | Ctrl + h 98 | 99 | 100 | Ctrl + k 101 | 102 | 103 | delete / backspace 104 | 105 | 106 | 107 | ); 108 | }; 109 | 110 | renderEdgeDetail = () => { 111 | const { 112 | label = '', 113 | shape = 'flow-polyline-round', 114 | color, 115 | style: { lineWidth }, 116 | } = this.item.getModel(); 117 | 118 | return ( 119 | <> 120 |
121 | 122 | 123 | 124 | 125 | 126 | 135 | 136 | 137 | 138 | 143 | this.handleFieldChange({ style: { lineWidth } }) 144 | } 145 | /> 146 | 147 | 148 | 149 | this.handleFieldChange({ color })} 152 | /> 153 | 154 |
155 | 161 | 162 | delete / backspace 163 | 164 | 165 | 166 | ); 167 | }; 168 | 169 | renderGroupDetail = () => { 170 | const { label = 'Group' } = this.item.getModel(); 171 | 172 | return ( 173 |
174 | 175 | 176 | 177 |
178 | ); 179 | }; 180 | 181 | render() { 182 | const { type } = this.props; 183 | if (!this.item) return null; 184 | 185 | return ( 186 | 193 | {type === 'node' && this.renderNodeDetail()} 194 | {type === 'edge' && this.renderEdgeDetail()} 195 | {type === 'group' && this.renderGroupDetail()} 196 | 197 | ); 198 | } 199 | } 200 | 201 | DetailForm.propTypes = { 202 | type: PropTypes.string.isRequired 203 | }; 204 | 205 | export default withPropsAPI(DetailForm); 206 | -------------------------------------------------------------------------------- /src/components/FlowDetailPanel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from 'antd/es/card'; 3 | import 'antd/es/card/style/css'; 4 | import Descriptions from 'antd/es/descriptions'; 5 | import 'antd/es/descriptions/style/css'; 6 | import { 7 | CanvasPanel, 8 | DetailPanel, 9 | EdgePanel, 10 | GroupPanel, 11 | MultiPanel, 12 | NodePanel, 13 | } from 'gg-editor'; 14 | 15 | import DetailForm from './DetailForm'; 16 | import './style.css'; 17 | 18 | const FlowDetailPanel = () => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 38 | 39 | 40 | 47 | 53 | 54 | Ctrl + = 55 | 56 | 57 | Ctrl + - 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default FlowDetailPanel; 67 | -------------------------------------------------------------------------------- /src/components/FlowDetailPanel/style.css: -------------------------------------------------------------------------------- 1 | .details__card { 2 | height: calc(100vh - 262px); 3 | } 4 | .ant-descriptions-title { 5 | color: rgba(0, 0, 0, 0.7); 6 | font-weight: 500; 7 | font-size: 14px; 8 | } 9 | code { 10 | color: #c41d7f; 11 | font-size: 13px; 12 | } 13 | 14 | .react-colorful { 15 | width: 100%; 16 | height: 150px; 17 | cursor: pointer; 18 | } 19 | 20 | .react-colorful__saturation, 21 | .react-colorful__hue { 22 | border-radius: 3px; 23 | } 24 | 25 | .react-colorful__saturation { 26 | margin-bottom: 10px; 27 | border-bottom: none; 28 | } 29 | 30 | .react-colorful__hue { 31 | height: 6px; 32 | } 33 | 34 | .react-colorful__saturation-pointer, 35 | .react-colorful__hue-pointer { 36 | width: 16px; 37 | height: 16px; 38 | z-index: auto; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/FlowItemPanel/NodeItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Item } from 'gg-editor'; 3 | 4 | const NodeItem = (props) => { 5 | const { label, img } = props; 6 | 7 | return ( 8 | 14 | ); 15 | }; 16 | 17 | export default NodeItem; 18 | -------------------------------------------------------------------------------- /src/components/FlowItemPanel/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ItemPanel } from 'gg-editor'; 3 | import { Card, Input } from 'antd/es'; 4 | import 'antd/es/card/style/css'; 5 | 6 | import NodeItem from './NodeItem'; 7 | import nodes from './nodesData'; 8 | import './style.css'; 9 | 10 | const FlowItemPanel = () => { 11 | const [items, setItems] = useState(nodes); 12 | 13 | function onChange(e) { 14 | const keyword = e.target.value.toLowerCase(); 15 | const searchResults = nodes.filter(n => n.label.toLowerCase().includes(keyword)); 16 | setItems(searchResults); 17 | } 18 | 19 | return ( 20 | 21 | 22 | 23 | {items && items.map((item, i) => )} 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default FlowItemPanel; 30 | -------------------------------------------------------------------------------- /src/components/FlowItemPanel/style.css: -------------------------------------------------------------------------------- 1 | .sidebar-wrapper { 2 | position: absolute; 3 | left: 0; 4 | top: 0; 5 | bottom: 0; 6 | box-shadow: 0px 0px 10px 0px rgba(77, 77, 77, 0.2); 7 | z-index: 999; 8 | } 9 | .sidebar { 10 | width: 112px; 11 | height: 100vh; 12 | user-select: none; 13 | } 14 | .sidebar .ant-card-body { 15 | position: fixed; 16 | top: 15px; 17 | left: 0; 18 | width: 112px; 19 | bottom: 0; 20 | height: 100vh; 21 | overflow: scroll; 22 | 23 | z-index: 1000; 24 | text-align: center; 25 | } 26 | .sidebar .ant-card-body div { 27 | padding: 4px 16px; 28 | transition: all 0.1s ease; 29 | background: #ffffff; 30 | position: relative; 31 | z-index: 1; 32 | } 33 | /* .sidebar .ant-card-body div:after { 34 | content: ''; 35 | position: relative; 36 | z-index: 9999; 37 | } */ 38 | .sidebar .ant-card-body div:after { 39 | content: ''; 40 | position: absolute; 41 | z-index: 9; 42 | left: 0; 43 | right: 0; 44 | top: 0; 45 | bottom: 0; 46 | } 47 | .sidebar .ant-card-body div:hover:after { 48 | content: ''; 49 | position: absolute; 50 | z-index: 999; 51 | left: 0; 52 | right: 0; 53 | top: 0; 54 | bottom: 0; 55 | } 56 | .sidebar .ant-card-body div:hover { 57 | box-shadow: 0 0 25px rgba(0, 0, 0, 0.4); 58 | transform: scale(1.15); 59 | z-index: 999; 60 | } 61 | .sidebar div img { 62 | max-width: 100%; 63 | margin-top: -10px; 64 | position: relative; 65 | z-index: 9999; 66 | pointer-events: none; 67 | } 68 | 69 | .sidebar .sidebar-search { 70 | margin: 5px; 71 | width: auto; 72 | } 73 | 74 | .sidebar .sidebar-search input { 75 | font-size: 10px; 76 | } 77 | ::-webkit-scrollbar { 78 | width: 0px; 79 | background: transparent; /* make scrollbar transparent */ 80 | } 81 | -------------------------------------------------------------------------------- /src/components/FlowMiniMap/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Card from 'antd/es/card'; 3 | import 'antd/es/card/style/css'; 4 | import { Minimap } from 'gg-editor'; 5 | 6 | const FlowMiniMap = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default FlowMiniMap; 15 | -------------------------------------------------------------------------------- /src/components/FlowToolbar/ToolbarButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Command } from 'gg-editor'; 3 | import Tooltip from 'antd/es/tooltip'; 4 | import 'antd/es/tooltip/style/css'; 5 | 6 | import { upperFirst } from '../../utils'; 7 | import IconFont from '../IconFont'; 8 | 9 | const ToolbarButton = (props) => { 10 | const { command, icon, text } = props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default ToolbarButton; 22 | -------------------------------------------------------------------------------- /src/components/FlowToolbar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Toolbar } from 'gg-editor'; 3 | import Divider from 'antd/es/divider'; 4 | import 'antd/es/divider/style/css'; 5 | 6 | import ToolbarButton from './ToolbarButton'; 7 | import './style.css'; 8 | 9 | const FlowToolbar = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default FlowToolbar; 43 | -------------------------------------------------------------------------------- /src/components/FlowToolbar/style.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | position: absolute; 3 | display: flex; 4 | z-index: 9999; 5 | background: #fff; 6 | padding: 5px 15px; 7 | bottom: 30px; 8 | transform: translateX(-50%); 9 | left: 50%; 10 | border-radius: 10px; 11 | box-shadow: 0 10px 20px rgba($color: #000000, $alpha: 0.1); 12 | } 13 | .toolbar .command span { 14 | padding: 12px; 15 | cursor: pointer; 16 | font-size: 18px; 17 | transition: all 0.3s ease; 18 | } 19 | 20 | .toolbar .command .ant-divider { 21 | height: auto; 22 | background: #e0e0e0; 23 | } 24 | .toolbar .command:hover span { 25 | transform: scale(1.4); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/IconFont/index.js: -------------------------------------------------------------------------------- 1 | import { createFromIconfontCN } from '@ant-design/icons'; 2 | 3 | const IconFont = createFromIconfontCN({ 4 | scriptUrl: 'https://at.alicdn.com/t/font_1794059_wia34skss5b.js', 5 | }); 6 | 7 | export default IconFont; 8 | -------------------------------------------------------------------------------- /src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from 'antd/es/layout'; 3 | import Row from 'antd/es/row'; 4 | import Col from 'antd/es/col'; 5 | import GGEditor from 'gg-editor'; 6 | 7 | import 'antd/es/layout/style/css'; 8 | import 'antd/es/row/style/css'; 9 | import 'antd/es/col/style/css'; 10 | 11 | import { 12 | NodeRegisteWithHeader, 13 | NodeRegisteWithoutHeader, 14 | } from '../register/node'; 15 | import FlowToolbar from '../../components/FlowToolbar'; 16 | import FlowCanvas from '../../components/FlowCanvas'; 17 | import FlowItemPanel from '../../components/FlowItemPanel'; 18 | import FlowDetailPanel from '../../components/FlowDetailPanel'; 19 | import FlowMiniMap from '../../components/FlowMiniMap'; 20 | import ExportCanvas from '../../components/ExportCanvas'; 21 | import { saveData } from '../../utils/saveData'; 22 | 23 | GGEditor.setTrackable(false); 24 | 25 | let counter = 0; 26 | 27 | const App = () => { 28 | function onBeforeCommandExecute(ev) { 29 | const { command } = ev; 30 | 31 | if (command.name !== 'add') return; 32 | 33 | const { addModel, type } = command; 34 | 35 | if (type === 'node') { 36 | addModel.shape = 'node-image-header'; 37 | } 38 | 39 | if (type === 'edge') { 40 | addModel.shape = 'flow-polyline-round'; 41 | addModel.color = '#a4b2c0'; 42 | addModel.style = { lineWidth: 2 }; 43 | } 44 | } 45 | 46 | function onAfterCommandExecute(ev) { 47 | if (!['toFront', 'toBack'].includes(ev.command.name)) return; 48 | 49 | // this event is triggered twice for every `toBack` or `toFront` 50 | // first time with outdated snapshot and second time with updated snapshot. 51 | // there's no way to tell them apart 52 | 53 | // if we setData with the outdated snapshot, it doesn't trigger the second time 54 | // and we don't get the updated snapshot 55 | 56 | // so using a dirty trick here 57 | if (counter % 2 === 1) saveData(ev.command.snapShot); 58 | counter += 1; 59 | } 60 | 61 | return ( 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default App; 87 | -------------------------------------------------------------------------------- /src/containers/register/node/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RegisterNode } from 'gg-editor'; 3 | 4 | const NodeRegisteWithoutHeader = () => { 5 | const config = { 6 | afterDraw(cfg) { 7 | const { group } = cfg; 8 | const size = cfg.size || [100, 100]; 9 | const width = size[0]; 10 | const height = size[1]; 11 | group.addShape('Path', { 12 | attrs: { 13 | fill: '#EBEFF0', 14 | stroke: '#BCC2C6', 15 | }, 16 | }); 17 | group.addShape('image', { 18 | // attrs: node image style 19 | attrs: { 20 | x: -(width - 3) / 2, 21 | y: -(height + 11) / 2, 22 | width: width - 3, 23 | height: height, 24 | img: group.model.img, 25 | }, 26 | draggable: true, 27 | }); 28 | }, 29 | drawLabel(t) {}, 30 | }; 31 | 32 | return ( 33 | 38 | ); 39 | }; 40 | 41 | const NodeRegisteWithHeader = () => { 42 | const config = { 43 | afterDraw(cfg) { 44 | const { group } = cfg; 45 | const size = cfg.size || [100, 100]; 46 | const width = size[0]; 47 | const height = size[1]; 48 | group.addShape('Path', { 49 | attrs: { 50 | fill: '#EBEFF0', 51 | stroke: '#BCC2C6', 52 | }, 53 | }); 54 | group.addShape('image', { 55 | // attrs: node image style 56 | attrs: { 57 | x: -(width - 3) / 2, 58 | y: -height / 2, 59 | width: width - 3, 60 | height: height, 61 | img: group.model.img, 62 | }, 63 | draggable: true, 64 | }); 65 | if (cfg.model.label) { 66 | group.addShape('text', { 67 | // attrs: label style 68 | attrs: { 69 | x: 0, 70 | y: -(height - 24) / 2, 71 | textAlign: 'center', 72 | fontWeight: 600, 73 | textBaseline: 'middle', 74 | text: (cfg.model.label.length > 23) ? cfg.model.label.substr(0, 20) + '...' : cfg.model.label, 75 | fill: '#94A4A5', 76 | fontSize: 10, 77 | }, 78 | }); 79 | } 80 | }, 81 | drawLabel(t) {}, 82 | }; 83 | 84 | return ( 85 | 86 | ); 87 | }; 88 | 89 | export { NodeRegisteWithHeader, NodeRegisteWithoutHeader }; 90 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import * as serviceWorker from './serviceWorker'; 5 | import App from './containers/App'; 6 | import './index.css'; 7 | 8 | const Container = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | ReactDOM.render(, document.getElementById('root')); 17 | 18 | if (module.hot) { 19 | module.hot.accept('./containers/App', () => { 20 | ReactDOM.render(, document.getElementById('root')); 21 | }); 22 | } 23 | 24 | // If you want your app to work offline and load faster, you can change 25 | // unregister() to register() below. Note this comes with some pitfalls. 26 | // Learn more about service workers: https://bit.ly/CRA-PWA 27 | serviceWorker.register(); 28 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | 6 | import '@testing-library/jest-dom/extend-expect'; -------------------------------------------------------------------------------- /src/utils/dataMapToData.js: -------------------------------------------------------------------------------- 1 | export function dataMapToData(dataMap) { 2 | const data = { nodes: [], edges: [], groups: [] }; 3 | if (dataMap) { 4 | Object.keys(dataMap).forEach((id) => { 5 | const item = dataMap[id]; 6 | if (item.type === 'node') { 7 | data.nodes.push(item); 8 | } else if (!!item.source && !!item.target) data.edges.push(item); 9 | // just assuming that if an item is not node or edge but have x and y, it's a group 10 | else if (!!item.x && !!item.y) { 11 | data.groups.push(item); 12 | } 13 | }); 14 | } 15 | 16 | return data; 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export const upperFirst = (str) => 2 | str && typeof str === "string" ? str.toLowerCase().replace(/( |^)[a-z]/g, (l) => l.toUpperCase()) : ""; 3 | -------------------------------------------------------------------------------- /src/utils/saveData.js: -------------------------------------------------------------------------------- 1 | export function saveData(data) { 2 | localStorage.setItem('data', JSON.stringify(data)); 3 | } 4 | --------------------------------------------------------------------------------