├── .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 | [](https://app.netlify.com/sites/wireflow-app/deploys)
4 | [](https://david-dm.org/vanila-io/wireflow)
5 | [](https://david-dm.org/vanila-io/wireflow?type=dev)
6 | [](#backers)
7 | [](#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 | 
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 |
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 |
62 |
--------------------------------------------------------------------------------
/src/assets/images/E-Commerce/Paypal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
81 |
--------------------------------------------------------------------------------
/src/assets/images/E-Commerce/Products 3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/src/assets/images/Features/Features 3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
76 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/404.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
60 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Analitycs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Counter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
73 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
70 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Faqs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
59 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Forum.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
60 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
68 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Progress.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
76 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
67 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Sitemap.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
58 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Steps.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
68 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Tags.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
67 |
--------------------------------------------------------------------------------
/src/assets/images/Misc/Under Contruction.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
77 |
--------------------------------------------------------------------------------
/src/assets/images/Multimedia/Songs 2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
80 |
--------------------------------------------------------------------------------
/src/assets/images/Multimedia/Songs 3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
64 |
--------------------------------------------------------------------------------
/src/assets/images/Sign in/Sign Up 1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
71 |
--------------------------------------------------------------------------------
/src/assets/images/Sign in/Sign in 1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
71 |
--------------------------------------------------------------------------------
/src/assets/images/Socials/Profile 2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
73 |
--------------------------------------------------------------------------------
/src/assets/images/Socials/Profile 3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | }
33 | />
34 |
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 |
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 |
155 |
161 |
162 | delete
/ backspace
163 |
164 |
165 | >
166 | );
167 | };
168 |
169 | renderGroupDetail = () => {
170 | const { label = 'Group' } = this.item.getModel();
171 |
172 | return (
173 |
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 |
--------------------------------------------------------------------------------