Compare commits
	
		
			42 Commits
		
	
	
		
			AUT-659
			...
			AUT-157-AU
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8f3ecb6d4d | ||
|   | 47caa5aa37 | ||
|   | 725b38c697 | ||
|   | 402a0fdf3b | ||
|   | 078364ffa1 | ||
|   | f64d5ec4fc | ||
|   | 12194a50e1 | ||
|   | 82ee592699 | ||
|   | 1b4fb2ce6e | ||
|   | ebea8d12d1 | ||
|   | f842dd77df | ||
|   | a6ec7a6c99 | ||
|   | 369c72282c | ||
|   | 6f30c1a509 | ||
|   | abfd1116c7 | ||
|   | 017854955d | ||
|   | 1405cddea1 | ||
|   | 00dd3164c9 | ||
|   | d5cbc0f611 | ||
|   | 5d2e9ccc67 | ||
|   | 017a881494 | ||
|   | 52994970e6 | ||
|   | ebae629e5c | ||
|   | 4d79220b0c | ||
|   | 96fba7fbb8 | ||
|   | e0d610071d | ||
|   | ab0966c005 | ||
|   | 751eb41e72 | ||
|   | f08dc25711 | ||
|   | 737eb31776 | ||
|   | d6abf283bc | ||
|   | bac4ab5aa4 | ||
|   | b5839390fd | ||
|   | d19271dae1 | ||
|   | ef5a09314e | ||
|   | ba52e298eb | ||
|   | b3c3998189 | ||
|   | 782f9b5c04 | ||
|   | 3079d8c605 | ||
|   | c5202d7b3e | ||
|   | fbae83f4de | ||
|   | 1dc9646894 | 
| @@ -1,21 +0,0 @@ | |||||||
| <!-- |  | ||||||
|   - maskable-icon.svg |  | ||||||
|   - Copyright (c) 2022 james@firefly-iii.org |  | ||||||
|   - |  | ||||||
|   - This file is part of Firefly III (https://github.com/firefly-iii). |  | ||||||
|   - |  | ||||||
|   - This program is free software: you can redistribute it and/or modify |  | ||||||
|   - it under the terms of the GNU Affero General Public License as |  | ||||||
|   - published by the Free Software Foundation, either version 3 of the |  | ||||||
|   - License, or (at your option) any later version. |  | ||||||
|   - |  | ||||||
|   - This program is distributed in the hope that it will be useful, |  | ||||||
|   - but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|   - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|   - GNU Affero General Public License for more details. |  | ||||||
|   - |  | ||||||
|   - You should have received a copy of the GNU Affero General Public License |  | ||||||
|   - along with this program.  If not, see <https://www.gnu.org/licenses/>. |  | ||||||
|   --> |  | ||||||
|  |  | ||||||
| <svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg> |  | ||||||
| Before Width: | Height: | Size: 2.9 KiB | 
| @@ -1,20 +0,0 @@ | |||||||
| import { URLSearchParams } from 'url'; |  | ||||||
|  |  | ||||||
| export default async function generateAuthUrl($) { |  | ||||||
|   const oauthRedirectUrlField = $.app.auth.fields.find( |  | ||||||
|     (field) => field.key == 'oAuthRedirectUrl' |  | ||||||
|   ); |  | ||||||
|   const redirectUri = oauthRedirectUrlField.value; |  | ||||||
|   const searchParams = new URLSearchParams({ |  | ||||||
|     client_id: $.auth.data.clientId, |  | ||||||
|     redirect_uri: redirectUri, |  | ||||||
|     response_type: 'code', |  | ||||||
|   }); |  | ||||||
|   const instanceUrl = $.auth.data.instanceUrl; |  | ||||||
|  |  | ||||||
|   const url = `${instanceUrl}/oauth/authorize?${searchParams.toString()}`; |  | ||||||
|  |  | ||||||
|   await $.auth.set({ |  | ||||||
|     url, |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| import generateAuthUrl from './generate-auth-url.js'; |  | ||||||
| import verifyCredentials from './verify-credentials.js'; |  | ||||||
| import refreshToken from './refresh-token.js'; |  | ||||||
| import isStillVerified from './is-still-verified.js'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   fields: [ |  | ||||||
|     { |  | ||||||
|       key: 'oAuthRedirectUrl', |  | ||||||
|       label: 'OAuth Redirect URL', |  | ||||||
|       type: 'string', |  | ||||||
|       required: true, |  | ||||||
|       readOnly: true, |  | ||||||
|       value: '{WEB_APP_URL}/app/firefly-iii/connections/add', |  | ||||||
|       placeholder: null, |  | ||||||
|       description: '', |  | ||||||
|       clickToCopy: true, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       key: 'instanceUrl', |  | ||||||
|       label: 'Instance URL', |  | ||||||
|       type: 'string', |  | ||||||
|       required: true, |  | ||||||
|       readOnly: false, |  | ||||||
|       value: null, |  | ||||||
|       placeholder: null, |  | ||||||
|       description: '', |  | ||||||
|       clickToCopy: true, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       key: 'clientId', |  | ||||||
|       label: 'Client ID', |  | ||||||
|       type: 'string', |  | ||||||
|       required: true, |  | ||||||
|       readOnly: false, |  | ||||||
|       value: null, |  | ||||||
|       placeholder: null, |  | ||||||
|       description: null, |  | ||||||
|       clickToCopy: false, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       key: 'clientSecret', |  | ||||||
|       label: 'Client Secret', |  | ||||||
|       type: 'string', |  | ||||||
|       required: true, |  | ||||||
|       readOnly: false, |  | ||||||
|       value: null, |  | ||||||
|       placeholder: null, |  | ||||||
|       description: null, |  | ||||||
|       clickToCopy: false, |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
|  |  | ||||||
|   generateAuthUrl, |  | ||||||
|   verifyCredentials, |  | ||||||
|   isStillVerified, |  | ||||||
|   refreshToken, |  | ||||||
| }; |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| import getCurrentUser from '../common/get-current-user.js'; |  | ||||||
|  |  | ||||||
| const isStillVerified = async ($) => { |  | ||||||
|   const currentUser = await getCurrentUser($); |  | ||||||
|   return !!currentUser.attributes.email; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default isStillVerified; |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| import { URLSearchParams } from 'node:url'; |  | ||||||
|  |  | ||||||
| const refreshToken = async ($) => { |  | ||||||
|   const params = new URLSearchParams({ |  | ||||||
|     client_id: $.auth.data.clientId, |  | ||||||
|     client_secret: $.auth.data.clientSecret, |  | ||||||
|     grant_type: 'refresh_token', |  | ||||||
|     refresh_token: $.auth.data.refreshToken, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const { data } = await $.http.post(`/oauth/token`, params.toString()); |  | ||||||
|  |  | ||||||
|   await $.auth.set({ |  | ||||||
|     accessToken: data.access_token, |  | ||||||
|     expiresIn: data.expires_in, |  | ||||||
|     tokenType: data.token_type, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default refreshToken; |  | ||||||
| @@ -1,35 +0,0 @@ | |||||||
| import getCurrentUser from '../common/get-current-user.js'; |  | ||||||
|  |  | ||||||
| const verifyCredentials = async ($) => { |  | ||||||
|   const oauthRedirectUrlField = $.app.auth.fields.find( |  | ||||||
|     (field) => field.key == 'oAuthRedirectUrl' |  | ||||||
|   ); |  | ||||||
|   const redirectUri = oauthRedirectUrlField.value; |  | ||||||
|   const { data } = await $.http.post('/oauth/token', { |  | ||||||
|     client_id: $.auth.data.clientId, |  | ||||||
|     client_secret: $.auth.data.clientSecret, |  | ||||||
|     code: $.auth.data.code, |  | ||||||
|     grant_type: 'authorization_code', |  | ||||||
|     redirect_uri: redirectUri, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   await $.auth.set({ |  | ||||||
|     accessToken: data.access_token, |  | ||||||
|     tokenType: data.token_type, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const currentUser = await getCurrentUser($); |  | ||||||
|  |  | ||||||
|   await $.auth.set({ |  | ||||||
|     clientId: $.auth.data.clientId, |  | ||||||
|     clientSecret: $.auth.data.clientSecret, |  | ||||||
|     scope: $.auth.data.scope, |  | ||||||
|     idToken: data.id_token, |  | ||||||
|     expiresIn: data.expires_in, |  | ||||||
|     refreshToken: data.refresh_token, |  | ||||||
|     resourceName: currentUser.resourceName, |  | ||||||
|     screenName: currentUser.attributes.email, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default verifyCredentials; |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| const addAuthHeader = ($, requestConfig) => { |  | ||||||
|   if ($.auth.data?.accessToken) { |  | ||||||
|     requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return requestConfig; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default addAuthHeader; |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| const getCurrentUser = async ($) => { |  | ||||||
|   const { data: currentUser } = await $.http.get('/api/v1/about/user', { |  | ||||||
|     Headers: { |  | ||||||
|       Accept: 'application/vnd.api+json', |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|   return currentUser.data; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default getCurrentUser; |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| const setBaseUrl = ($, requestConfig) => { |  | ||||||
|   const instanceUrl = $.auth.data.instanceUrl; |  | ||||||
|   if (instanceUrl) { |  | ||||||
|     requestConfig.baseURL = instanceUrl; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return requestConfig; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default setBaseUrl; |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| import defineApp from '../../helpers/define-app.js'; |  | ||||||
| import addAuthHeader from './common/add-auth-header.js'; |  | ||||||
| import auth from './auth/index.js'; |  | ||||||
| import setBaseUrl from './common/set-base-url.js'; |  | ||||||
|  |  | ||||||
| export default defineApp({ |  | ||||||
|   name: 'Firefly III', |  | ||||||
|   key: 'firefly-iii', |  | ||||||
|   baseUrl: '', |  | ||||||
|   apiBaseUrl: '', |  | ||||||
|   iconUrl: '{BASE_URL}/apps/firefly-iii/assets/favicon.svg', |  | ||||||
|   authDocUrl: '{DOCS_URL}/apps/firefly-iii/connection', |  | ||||||
|   primaryColor: 'CD5029', |  | ||||||
|   supportsConnections: true, |  | ||||||
|   beforeRequest: [setBaseUrl, addAuthHeader], |  | ||||||
|   auth, |  | ||||||
| }); |  | ||||||
| @@ -52,7 +52,7 @@ const appConfig = { | |||||||
|   isDev: appEnv === 'development', |   isDev: appEnv === 'development', | ||||||
|   isTest: appEnv === 'test', |   isTest: appEnv === 'test', | ||||||
|   isProd: appEnv === 'production', |   isProd: appEnv === 'production', | ||||||
|   version: '0.11.0', |   version: '0.12.0', | ||||||
|   postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', |   postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', | ||||||
|   postgresSchema: process.env.POSTGRES_SCHEMA || 'public', |   postgresSchema: process.env.POSTGRES_SCHEMA || 'public', | ||||||
|   postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), |   postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => { | |||||||
|  |  | ||||||
|     const expectedPayload = { |     const expectedPayload = { | ||||||
|       data: { |       data: { | ||||||
|         version: '0.11.0', |         version: '0.12.0', | ||||||
|       }, |       }, | ||||||
|       meta: { |       meta: { | ||||||
|         count: 1, |         count: 1, | ||||||
|   | |||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | export async function up(knex) { | ||||||
|  |   return knex.schema.alterTable('datastore', (table) => { | ||||||
|  |     table.text('value').alter(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export async function down(knex) { | ||||||
|  |   return knex.schema.alterTable('datastore', (table) => { | ||||||
|  |     table.string('value').alter(); | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -33,8 +33,8 @@ class User extends Base { | |||||||
|       fullName: { type: 'string', minLength: 1 }, |       fullName: { type: 'string', minLength: 1 }, | ||||||
|       email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, |       email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, | ||||||
|       password: { type: 'string' }, |       password: { type: 'string' }, | ||||||
|       resetPasswordToken: { type: 'string' }, |       resetPasswordToken: { type: ['string', 'null'] }, | ||||||
|       resetPasswordTokenSentAt: { type: 'string' }, |       resetPasswordTokenSentAt: { type: ['string', 'null'], format: 'date-time' }, | ||||||
|       trialExpiryDate: { type: 'string' }, |       trialExpiryDate: { type: 'string' }, | ||||||
|       roleId: { type: 'string', format: 'uuid' }, |       roleId: { type: 'string', format: 'uuid' }, | ||||||
|       deletedAt: { type: 'string' }, |       deletedAt: { type: 'string' }, | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ export const worker = new Worker( | |||||||
|       await user.$relatedQuery('usageData').withSoftDeleted().hardDelete(); |       await user.$relatedQuery('usageData').withSoftDeleted().hardDelete(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete(); | ||||||
|     await user.$query().withSoftDeleted().hardDelete(); |     await user.$query().withSoftDeleted().hardDelete(); | ||||||
|   }, |   }, | ||||||
|   { connection: redisConfig } |   { connection: redisConfig } | ||||||
|   | |||||||
| @@ -122,12 +122,6 @@ export default defineConfig({ | |||||||
|             { text: 'Connection', link: '/apps/filter/connection' }, |             { text: 'Connection', link: '/apps/filter/connection' }, | ||||||
|           ], |           ], | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|           text: 'Firefly III', |  | ||||||
|           collapsible: true, |  | ||||||
|           collapsed: true, |  | ||||||
|           items: [{ text: 'Connection', link: '/apps/firefly-iii/connection' }], |  | ||||||
|         }, |  | ||||||
|         { |         { | ||||||
|           text: 'Flickr', |           text: 'Flickr', | ||||||
|           collapsible: true, |           collapsible: true, | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| # Firefly III |  | ||||||
|  |  | ||||||
| :::info |  | ||||||
| This page explains the steps you need to follow to set up the Firefly III |  | ||||||
| connection in Automatisch. If any of the steps are outdated, please let us know! |  | ||||||
| ::: |  | ||||||
|  |  | ||||||
| 1. Go to your dashboard. |  | ||||||
| 2. Click on the **Options** tab on the left, and click on the **Profile** button. |  | ||||||
| 3. Go to **Oauth** tab and click on the **Create New Client** button. |  | ||||||
| 4. Enter a name for your project. |  | ||||||
| 5. Copy **OAuth Redirect URL** from Automatisch to **Redirect URL** field, and click on the **Create** button. |  | ||||||
| 6. Copy the **Client ID** value from the following popup to the `Client ID` field on Automatisch. |  | ||||||
| 7. Copy the **Secret** value from the following popup to the `Client Secret` field on Automatisch. |  | ||||||
| 8. Fill **Instance URL** with your Firefly III instance url. |  | ||||||
| 9. Click **Submit** button on Automatisch. |  | ||||||
| 10. Congrats! Start using your new Firefly III connection within the flows. |  | ||||||
| @@ -6,16 +6,12 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the | |||||||
| . | . | ||||||
| ├── packages | ├── packages | ||||||
| │   ├── backend | │   ├── backend | ||||||
| │   ├── cli |  | ||||||
| │   ├── docs | │   ├── docs | ||||||
| │   ├── e2e-tests | │   ├── e2e-tests | ||||||
| │   ├── types |  | ||||||
| │   └── web | │   └── web | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| - `backend` - The backend package contains the backend application and all integrations. | - `backend` - The backend package contains the backend application and all integrations. | ||||||
| - `cli` - The cli package contains the CLI application of Automatisch. |  | ||||||
| - `docs` - The docs package contains the documentation website. | - `docs` - The docs package contains the documentation website. | ||||||
| - `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage. | - `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage. | ||||||
| - `types` - The types package contains the shared types for both the backend and web packages. |  | ||||||
| - `web` - The web package contains the frontend application of Automatisch. | - `web` - The web package contains the frontend application of Automatisch. | ||||||
|   | |||||||
| @@ -1,21 +0,0 @@ | |||||||
| <!-- |  | ||||||
|   - maskable-icon.svg |  | ||||||
|   - Copyright (c) 2022 james@firefly-iii.org |  | ||||||
|   - |  | ||||||
|   - This file is part of Firefly III (https://github.com/firefly-iii). |  | ||||||
|   - |  | ||||||
|   - This program is free software: you can redistribute it and/or modify |  | ||||||
|   - it under the terms of the GNU Affero General Public License as |  | ||||||
|   - published by the Free Software Foundation, either version 3 of the |  | ||||||
|   - License, or (at your option) any later version. |  | ||||||
|   - |  | ||||||
|   - This program is distributed in the hope that it will be useful, |  | ||||||
|   - but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
|   - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
|   - GNU Affero General Public License for more details. |  | ||||||
|   - |  | ||||||
|   - You should have received a copy of the GNU Affero General Public License |  | ||||||
|   - along with this program.  If not, see <https://www.gnu.org/licenses/>. |  | ||||||
|   --> |  | ||||||
|  |  | ||||||
| <svg height="377.95276" width="377.95276" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h377.95276v377.95276h-377.95276z" fill="#cd5029" stroke-width="1.96129"/><g transform="matrix(.77452773 0 0 .77452773 21.636074 21.374655)"><path d="m140.49013 78.646381 2.249 53.017999s-40.103 29.566-45.538 68l-16.001 1.231s-11.539 2.564-11.539 14.103v37.18s3.846 11.538 12.82 11.538l16.487-.319s8 30.5 36.5 50.5v25.5s-2 8.5 15.5 11 40.75 2.25 44.5-1.5 3.75-4.5 3.75-9c0 0 21.25 5 60.25 0v5s3.5 7 29 7 33-3 37.5-12v-25s37.009-36.264 35.75-91.75c-1.083-47.75-15.901-64.299-35.806-82.96-22.67-21.254-69.944-31.165-117.944-25.353.001-.001-24.341-43.937999-67.478-36.187999z" fill="#fff"/><circle cx="135.46912" cy="214.39638" fill="#cd5029" r="9.5"/><path d="m360.08113 190.51238s-18.218-8.742-40.662 3.996c0 0-26.711-8.987-40.99 2.593-14.828 12.025-16.299 26.115-15.525 42.785 0 0 12.837-43.915 45.252-32.571 0 0-22.947 40.43 12.761 47.508 0 0 8.436-.05 15.401-4.256 6.644-4.011 11.842-11.433 9.711-24.814 0 0-4.348-13.336-15.569-21.42 0 0 11.042-7.806 31.988-2.209z" fill="#cd5029"/><path d="m320.19013 213.01938s-16.689 31.461 5.607 29.767c0 0 11.838-5.656 4.887-17.127-7.147-11.796-10.494-12.64-10.494-12.64z" fill="#fff"/></g><path d="m188.97638 175.70052s4.01698 13.60604-3.69586 21.52748c-7.713 7.92145-6.8792 16.6767-3.75227 20.84588 3.12692 4.16917 2.91831 7.29593.41674 9.58905-2.50141 2.29312-4.58608 3.96073-6.04523.20846-1.45916-3.75228-3.12676-3.75228-3.75228-5.62834-.62552-1.87605-1.87622-5.21142-1.87622-5.21142s-3.96072 6.25384-6.46229 10.00611c-2.50157 3.75228-2.50141 9.58922-.83381 12.71598 1.66761 3.12676 1.04226 6.87903-.20845 12.09046-1.2507 5.21143.4169 13.13288 6.25369 16.2598 5.83678 3.12692 12.92459 5.62833 16.05135 8.5468s10.42301 5.62833 19.80362 3.54382c9.3806-2.0845 21.26294-11.67355 23.34744-18.13585 0 0 5.41988-6.04523 4.37763-13.96668s-4.79469-7.71316-6.4623-13.75839c-1.6676-6.04523 3.60854-4.55469-.8338-14.93382 0 0-1.98012-4.94005-9.50352-8.49899-4.83404-2.28661-1.54469-12.63061-10.09149-23.05347s-16.73295-12.14688-16.73295-12.14688z" fill="#ffa284" stroke-width=".162598"/></svg> |  | ||||||
| Before Width: | Height: | Size: 2.9 KiB | 
| @@ -7,6 +7,7 @@ | |||||||
|     "@apollo/client": "^3.6.9", |     "@apollo/client": "^3.6.9", | ||||||
|     "@casl/ability": "^6.5.0", |     "@casl/ability": "^6.5.0", | ||||||
|     "@casl/react": "^3.1.0", |     "@casl/react": "^3.1.0", | ||||||
|  |     "@dagrejs/dagre": "^1.1.2", | ||||||
|     "@emotion/react": "^11.4.1", |     "@emotion/react": "^11.4.1", | ||||||
|     "@emotion/styled": "^11.3.0", |     "@emotion/styled": "^11.3.0", | ||||||
|     "@hookform/resolvers": "^2.8.8", |     "@hookform/resolvers": "^2.8.8", | ||||||
| @@ -32,6 +33,7 @@ | |||||||
|     "react-router-dom": "^6.0.2", |     "react-router-dom": "^6.0.2", | ||||||
|     "react-scripts": "5.0.0", |     "react-scripts": "5.0.0", | ||||||
|     "react-window": "^1.8.9", |     "react-window": "^1.8.9", | ||||||
|  |     "reactflow": "^11.11.2", | ||||||
|     "slate": "^0.94.1", |     "slate": "^0.94.1", | ||||||
|     "slate-history": "^0.93.0", |     "slate-history": "^0.93.0", | ||||||
|     "slate-react": "^0.94.2", |     "slate-react": "^0.94.2", | ||||||
|   | |||||||
| @@ -68,7 +68,10 @@ function AccountDropdownMenu(props) { | |||||||
| AccountDropdownMenu.propTypes = { | AccountDropdownMenu.propTypes = { | ||||||
|   open: PropTypes.bool.isRequired, |   open: PropTypes.bool.isRequired, | ||||||
|   onClose: PropTypes.func.isRequired, |   onClose: PropTypes.func.isRequired, | ||||||
|   anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), |   anchorEl: PropTypes.oneOfType([ | ||||||
|  |     PropTypes.func, | ||||||
|  |     PropTypes.shape({ current: PropTypes.instanceOf(Element) }), | ||||||
|  |   ]), | ||||||
|   id: PropTypes.string.isRequired, |   id: PropTypes.string.isRequired, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ function AdminApplicationSettings(props) { | |||||||
|  |  | ||||||
|   const handleSubmit = async (values) => { |   const handleSubmit = async (values) => { | ||||||
|     try { |     try { | ||||||
|       if (!appConfig.data) { |       if (!appConfig?.data) { | ||||||
|         await createAppConfig({ |         await createAppConfig({ | ||||||
|           variables: { |           variables: { | ||||||
|             input: { key: props.appKey, ...values }, |             input: { key: props.appKey, ...values }, | ||||||
| @@ -69,6 +69,7 @@ function AdminApplicationSettings(props) { | |||||||
|     }), |     }), | ||||||
|     [appConfig?.data], |     [appConfig?.data], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Form |     <Form | ||||||
|       defaultValues={defaultValues} |       defaultValues={defaultValues} | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import * as URLS from 'config/urls'; | |||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
| import { ConnectionPropType } from 'propTypes/propTypes'; | import { ConnectionPropType } from 'propTypes/propTypes'; | ||||||
| import { useQueryClient } from '@tanstack/react-query'; | import { useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import Can from 'components/Can'; | ||||||
|  |  | ||||||
| function ContextMenu(props) { | function ContextMenu(props) { | ||||||
|   const { |   const { | ||||||
| @@ -44,21 +45,35 @@ function ContextMenu(props) { | |||||||
|       hideBackdrop={false} |       hideBackdrop={false} | ||||||
|       anchorEl={anchorEl} |       anchorEl={anchorEl} | ||||||
|     > |     > | ||||||
|  |       <Can I="read" a="Flow" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|           <MenuItem |           <MenuItem | ||||||
|             component={Link} |             component={Link} | ||||||
|             to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)} |             to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)} | ||||||
|             onClick={createActionHandler({ type: 'viewFlows' })} |             onClick={createActionHandler({ type: 'viewFlows' })} | ||||||
|  |             disabled={!allowed} | ||||||
|           > |           > | ||||||
|             {formatMessage('connection.viewFlows')} |             {formatMessage('connection.viewFlows')} | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|  |  | ||||||
|       <MenuItem onClick={createActionHandler({ type: 'test' })}> |       <Can I="update" a="Connection" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|  |           <MenuItem | ||||||
|  |             onClick={createActionHandler({ type: 'test' })} | ||||||
|  |             disabled={!allowed} | ||||||
|  |           > | ||||||
|             {formatMessage('connection.testConnection')} |             {formatMessage('connection.testConnection')} | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|  |  | ||||||
|  |       <Can I="create" a="Connection" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|           <MenuItem |           <MenuItem | ||||||
|             component={Link} |             component={Link} | ||||||
|         disabled={disableReconnection} |             disabled={!allowed || disableReconnection} | ||||||
|             to={URLS.APP_RECONNECT_CONNECTION( |             to={URLS.APP_RECONNECT_CONNECTION( | ||||||
|               appKey, |               appKey, | ||||||
|               connection.id, |               connection.id, | ||||||
| @@ -68,10 +83,19 @@ function ContextMenu(props) { | |||||||
|           > |           > | ||||||
|             {formatMessage('connection.reconnect')} |             {formatMessage('connection.reconnect')} | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|  |  | ||||||
|       <MenuItem onClick={createActionHandler({ type: 'delete' })}> |       <Can I="delete" a="Connection" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|  |           <MenuItem | ||||||
|  |             onClick={createActionHandler({ type: 'delete' })} | ||||||
|  |             disabled={!allowed} | ||||||
|  |           > | ||||||
|             {formatMessage('connection.delete')} |             {formatMessage('connection.delete')} | ||||||
|           </MenuItem> |           </MenuItem> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|     </Menu> |     </Menu> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; | |||||||
|  |  | ||||||
| import AppConnectionRow from 'components/AppConnectionRow'; | import AppConnectionRow from 'components/AppConnectionRow'; | ||||||
| import NoResultFound from 'components/NoResultFound'; | import NoResultFound from 'components/NoResultFound'; | ||||||
|  | import Can from 'components/Can'; | ||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
| import * as URLS from 'config/urls'; | import * as URLS from 'config/urls'; | ||||||
| import useAppConnections from 'hooks/useAppConnections'; | import useAppConnections from 'hooks/useAppConnections'; | ||||||
| @@ -16,11 +17,15 @@ function AppConnections(props) { | |||||||
|  |  | ||||||
|   if (!hasConnections) { |   if (!hasConnections) { | ||||||
|     return ( |     return ( | ||||||
|  |       <Can I="create" a="Connection" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|           <NoResultFound |           <NoResultFound | ||||||
|         to={URLS.APP_ADD_CONNECTION(appKey)} |  | ||||||
|             text={formatMessage('app.noConnections')} |             text={formatMessage('app.noConnections')} | ||||||
|             data-test="connections-no-results" |             data-test="connections-no-results" | ||||||
|  |             {...(allowed && { to: URLS.APP_ADD_CONNECTION(appKey) })} | ||||||
|           /> |           /> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import PaginationItem from '@mui/material/PaginationItem'; | |||||||
|  |  | ||||||
| import * as URLS from 'config/urls'; | import * as URLS from 'config/urls'; | ||||||
| import AppFlowRow from 'components/FlowRow'; | import AppFlowRow from 'components/FlowRow'; | ||||||
|  | import Can from 'components/Can'; | ||||||
| import NoResultFound from 'components/NoResultFound'; | import NoResultFound from 'components/NoResultFound'; | ||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
| import useConnectionFlows from 'hooks/useConnectionFlows'; | import useConnectionFlows from 'hooks/useConnectionFlows'; | ||||||
| @@ -36,11 +37,20 @@ function AppFlows(props) { | |||||||
|  |  | ||||||
|   if (!hasFlows) { |   if (!hasFlows) { | ||||||
|     return ( |     return ( | ||||||
|  |       <Can I="create" a="Flow" passThrough> | ||||||
|  |         {(allowed) => ( | ||||||
|           <NoResultFound |           <NoResultFound | ||||||
|         to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)} |  | ||||||
|             text={formatMessage('app.noFlows')} |             text={formatMessage('app.noFlows')} | ||||||
|             data-test="flows-no-results" |             data-test="flows-no-results" | ||||||
|  |             {...(allowed && { | ||||||
|  |               to: URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION( | ||||||
|  |                 appKey, | ||||||
|  |                 connectionId | ||||||
|  |               ), | ||||||
|  |             })} | ||||||
|           /> |           /> | ||||||
|  |         )} | ||||||
|  |       </Can> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -165,6 +165,7 @@ function ChooseAppAndEventSubstep(props) { | |||||||
|             value={getOption(appOptions, step.appKey) || null} |             value={getOption(appOptions, step.appKey) || null} | ||||||
|             onChange={onAppChange} |             onChange={onAppChange} | ||||||
|             data-test="choose-app-autocomplete" |             data-test="choose-app-autocomplete" | ||||||
|  |             componentsProps={{ popper: { className: 'nowheel' } }} | ||||||
|           /> |           /> | ||||||
|  |  | ||||||
|           {step.appKey && ( |           {step.appKey && ( | ||||||
| @@ -227,6 +228,7 @@ function ChooseAppAndEventSubstep(props) { | |||||||
|                 value={getOption(actionOrTriggerOptions, step.key) || null} |                 value={getOption(actionOrTriggerOptions, step.key) || null} | ||||||
|                 onChange={onEventChange} |                 onChange={onEventChange} | ||||||
|                 data-test="choose-event-autocomplete" |                 data-test="choose-event-autocomplete" | ||||||
|  |                 componentsProps={{ popper: { className: 'nowheel' } }} | ||||||
|               /> |               /> | ||||||
|             </Box> |             </Box> | ||||||
|           )} |           )} | ||||||
|   | |||||||
| @@ -240,6 +240,7 @@ function ChooseConnectionSubstep(props) { | |||||||
|             onChange={handleChange} |             onChange={handleChange} | ||||||
|             loading={isAppConnectionsLoading} |             loading={isAppConnectionsLoading} | ||||||
|             data-test="choose-connection-autocomplete" |             data-test="choose-connection-autocomplete" | ||||||
|  |             componentsProps={{ popper: { className: 'nowheel' } }} | ||||||
|           /> |           /> | ||||||
|  |  | ||||||
|           <Button |           <Button | ||||||
|   | |||||||
| @@ -32,9 +32,11 @@ function ControlledAutocomplete(props) { | |||||||
|     ...autocompleteProps |     ...autocompleteProps | ||||||
|   } = props; |   } = props; | ||||||
|   let dependsOnValues = []; |   let dependsOnValues = []; | ||||||
|  |  | ||||||
|   if (dependsOn?.length) { |   if (dependsOn?.length) { | ||||||
|     dependsOnValues = watch(dependsOn); |     dependsOnValues = watch(dependsOn); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     const hasDependencies = dependsOnValues.length; |     const hasDependencies = dependsOnValues.length; | ||||||
|     const allDepsSatisfied = dependsOnValues.every(Boolean); |     const allDepsSatisfied = dependsOnValues.every(Boolean); | ||||||
| @@ -44,6 +46,7 @@ function ControlledAutocomplete(props) { | |||||||
|       resetField(name); |       resetField(name); | ||||||
|     } |     } | ||||||
|   }, dependsOnValues); |   }, dependsOnValues); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Controller |     <Controller | ||||||
|       rules={{ required }} |       rules={{ required }} | ||||||
|   | |||||||
| @@ -21,7 +21,9 @@ const CustomOptions = (props) => { | |||||||
|     label, |     label, | ||||||
|     initialTabIndex, |     initialTabIndex, | ||||||
|   } = props; |   } = props; | ||||||
|  |  | ||||||
|   const [activeTabIndex, setActiveTabIndex] = React.useState(undefined); |   const [activeTabIndex, setActiveTabIndex] = React.useState(undefined); | ||||||
|  |  | ||||||
|   React.useEffect( |   React.useEffect( | ||||||
|     function applyInitialActiveTabIndex() { |     function applyInitialActiveTabIndex() { | ||||||
|       setActiveTabIndex((currentActiveTabIndex) => { |       setActiveTabIndex((currentActiveTabIndex) => { | ||||||
| @@ -33,6 +35,7 @@ const CustomOptions = (props) => { | |||||||
|     }, |     }, | ||||||
|     [initialTabIndex], |     [initialTabIndex], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Popper |     <Popper | ||||||
|       open={open} |       open={open} | ||||||
| @@ -47,6 +50,7 @@ const CustomOptions = (props) => { | |||||||
|           }, |           }, | ||||||
|         }, |         }, | ||||||
|       ]} |       ]} | ||||||
|  |       className="nowheel" | ||||||
|     > |     > | ||||||
|       <Paper elevation={5} sx={{ width: '100%' }}> |       <Paper elevation={5} sx={{ width: '100%' }}> | ||||||
|         <Tabs |         <Tabs | ||||||
| @@ -75,7 +79,10 @@ const CustomOptions = (props) => { | |||||||
|  |  | ||||||
| CustomOptions.propTypes = { | CustomOptions.propTypes = { | ||||||
|   open: PropTypes.bool.isRequired, |   open: PropTypes.bool.isRequired, | ||||||
|   anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, |   anchorEl: PropTypes.oneOfType([ | ||||||
|  |     PropTypes.func, | ||||||
|  |     PropTypes.shape({ current: PropTypes.instanceOf(Element) }), | ||||||
|  |   ]), | ||||||
|   data: PropTypes.arrayOf( |   data: PropTypes.arrayOf( | ||||||
|     PropTypes.shape({ |     PropTypes.shape({ | ||||||
|       id: PropTypes.string.isRequired, |       id: PropTypes.string.isRequired, | ||||||
|   | |||||||
| @@ -61,6 +61,7 @@ function ControlledCustomAutocomplete(props) { | |||||||
|   const [isSingleChoice, setSingleChoice] = React.useState(undefined); |   const [isSingleChoice, setSingleChoice] = React.useState(undefined); | ||||||
|   const priorStepsWithExecutions = React.useContext(StepExecutionsContext); |   const priorStepsWithExecutions = React.useContext(StepExecutionsContext); | ||||||
|   const editorRef = React.useRef(null); |   const editorRef = React.useRef(null); | ||||||
|  |   const mountedRef = React.useRef(false); | ||||||
|  |  | ||||||
|   const renderElement = React.useCallback( |   const renderElement = React.useCallback( | ||||||
|     (props) => <Element {...props} disabled={disabled} />, |     (props) => <Element {...props} disabled={disabled} />, | ||||||
| @@ -94,11 +95,15 @@ function ControlledCustomAutocomplete(props) { | |||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|  |     if (mountedRef.current) { | ||||||
|       const hasDependencies = dependsOnValues.length; |       const hasDependencies = dependsOnValues.length; | ||||||
|       if (hasDependencies) { |       if (hasDependencies) { | ||||||
|         // Reset the field when a dependent has been updated |         // Reset the field when a dependent has been updated | ||||||
|         resetEditor(editor); |         resetEditor(editor); | ||||||
|       } |       } | ||||||
|  |     } else { | ||||||
|  |       mountedRef.current = true; | ||||||
|  |     } | ||||||
|   }, dependsOnValues); |   }, dependsOnValues); | ||||||
|  |  | ||||||
|   React.useEffect( |   React.useEffect( | ||||||
|   | |||||||
| @@ -64,11 +64,19 @@ function DynamicField(props) { | |||||||
|           <Stack |           <Stack | ||||||
|             direction={{ xs: 'column', sm: 'row' }} |             direction={{ xs: 'column', sm: 'row' }} | ||||||
|             spacing={{ xs: 2 }} |             spacing={{ xs: 2 }} | ||||||
|             sx={{ display: 'flex', flex: 1 }} |             sx={{ | ||||||
|  |               display: 'flex', | ||||||
|  |               flex: 1, | ||||||
|  |               minWidth: 0, | ||||||
|  |             }} | ||||||
|           > |           > | ||||||
|             {fields.map((fieldSchema, fieldSchemaIndex) => ( |             {fields.map((fieldSchema, fieldSchemaIndex) => ( | ||||||
|               <Box |               <Box | ||||||
|                 sx={{ display: 'flex', flex: '1 0 0px' }} |                 sx={{ | ||||||
|  |                   display: 'flex', | ||||||
|  |                   flex: '1 0 0px', | ||||||
|  |                   minWidth: 0, | ||||||
|  |                 }} | ||||||
|                 key={`field-${field.__id}-${fieldSchemaIndex}`} |                 key={`field-${field.__id}-${fieldSchemaIndex}`} | ||||||
|               > |               > | ||||||
|                 <InputCreator |                 <InputCreator | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip'; | |||||||
| import IconButton from '@mui/material/IconButton'; | import IconButton from '@mui/material/IconButton'; | ||||||
| import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; | import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; | ||||||
| import Snackbar from '@mui/material/Snackbar'; | import Snackbar from '@mui/material/Snackbar'; | ||||||
|  | import { ReactFlowProvider } from 'reactflow'; | ||||||
|  |  | ||||||
| import { EditorProvider } from 'contexts/Editor'; | import { EditorProvider } from 'contexts/Editor'; | ||||||
| import EditableTypography from 'components/EditableTypography'; | import EditableTypography from 'components/EditableTypography'; | ||||||
| @@ -20,6 +21,9 @@ import * as URLS from 'config/urls'; | |||||||
| import { TopBar } from './style'; | import { TopBar } from './style'; | ||||||
| import useFlow from 'hooks/useFlow'; | import useFlow from 'hooks/useFlow'; | ||||||
| import { useQueryClient } from '@tanstack/react-query'; | import { useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import EditorNew from 'components/EditorNew/EditorNew'; | ||||||
|  |  | ||||||
|  | const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true'; | ||||||
|  |  | ||||||
| export default function EditorLayout() { | export default function EditorLayout() { | ||||||
|   const { flowId } = useParams(); |   const { flowId } = useParams(); | ||||||
| @@ -55,6 +59,7 @@ export default function EditorLayout() { | |||||||
|  |  | ||||||
|   const onFlowStatusUpdate = React.useCallback( |   const onFlowStatusUpdate = React.useCallback( | ||||||
|     async (active) => { |     async (active) => { | ||||||
|  |       try { | ||||||
|         await updateFlowStatus({ |         await updateFlowStatus({ | ||||||
|           variables: { |           variables: { | ||||||
|             input: { |             input: { | ||||||
| @@ -72,6 +77,7 @@ export default function EditorLayout() { | |||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); |         await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); | ||||||
|  |       } catch (err) {} | ||||||
|     }, |     }, | ||||||
|     [flowId, queryClient], |     [flowId, queryClient], | ||||||
|   ); |   ); | ||||||
| @@ -131,15 +137,28 @@ export default function EditorLayout() { | |||||||
|           </Button> |           </Button> | ||||||
|         </Box> |         </Box> | ||||||
|       </TopBar> |       </TopBar> | ||||||
|  |  | ||||||
|  |       {useNewFlowEditor ? ( | ||||||
|  |         <Stack direction="column" height="100%" flexGrow={1}> | ||||||
|  |           <Stack direction="column" flexGrow={1}> | ||||||
|  |             <EditorProvider value={{ readOnly: !!flow?.active }}> | ||||||
|  |               <ReactFlowProvider> | ||||||
|  |                 {!flow && !isFlowLoading && 'not found'} | ||||||
|  |                 {flow && <EditorNew flow={flow} />} | ||||||
|  |               </ReactFlowProvider> | ||||||
|  |             </EditorProvider> | ||||||
|  |           </Stack> | ||||||
|  |         </Stack> | ||||||
|  |       ) : ( | ||||||
|         <Stack direction="column" height="100%"> |         <Stack direction="column" height="100%"> | ||||||
|           <Container maxWidth="md"> |           <Container maxWidth="md"> | ||||||
|             <EditorProvider value={{ readOnly: !!flow?.active }}> |             <EditorProvider value={{ readOnly: !!flow?.active }}> | ||||||
|               {!flow && !isFlowLoading && 'not found'} |               {!flow && !isFlowLoading && 'not found'} | ||||||
|  |  | ||||||
|               {flow && <Editor flow={flow} />} |               {flow && <Editor flow={flow} />} | ||||||
|             </EditorProvider> |             </EditorProvider> | ||||||
|           </Container> |           </Container> | ||||||
|         </Stack> |         </Stack> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|       <Snackbar |       <Snackbar | ||||||
|         data-test="flow-cannot-edit-info-snackbar" |         data-test="flow-cannot-edit-info-snackbar" | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								packages/web/src/components/EditorNew/Edge/Edge.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								packages/web/src/components/EditorNew/Edge/Edge.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import { EdgeLabelRenderer, getStraightPath } from 'reactflow'; | ||||||
|  | import IconButton from '@mui/material/IconButton'; | ||||||
|  | import AddIcon from '@mui/icons-material/Add'; | ||||||
|  |  | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  | import { useContext } from 'react'; | ||||||
|  | import { EdgesContext } from '../EditorNew'; | ||||||
|  |  | ||||||
|  | export default function Edge({ | ||||||
|  |   sourceX, | ||||||
|  |   sourceY, | ||||||
|  |   targetX, | ||||||
|  |   targetY, | ||||||
|  |   source, | ||||||
|  |   data: { laidOut }, | ||||||
|  | }) { | ||||||
|  |   const { stepCreationInProgress, flowActive, onAddStep } = | ||||||
|  |     useContext(EdgesContext); | ||||||
|  |  | ||||||
|  |   const [edgePath, labelX, labelY] = getStraightPath({ | ||||||
|  |     sourceX, | ||||||
|  |     sourceY, | ||||||
|  |     targetX, | ||||||
|  |     targetY, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <EdgeLabelRenderer> | ||||||
|  |         <IconButton | ||||||
|  |           onClick={() => onAddStep(source)} | ||||||
|  |           color="primary" | ||||||
|  |           sx={{ | ||||||
|  |             position: 'absolute', | ||||||
|  |             transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, | ||||||
|  |             pointerEvents: 'all', | ||||||
|  |             visibility: laidOut ? 'visible' : 'hidden', | ||||||
|  |           }} | ||||||
|  |           disabled={stepCreationInProgress || flowActive} | ||||||
|  |         > | ||||||
|  |           <AddIcon /> | ||||||
|  |         </IconButton> | ||||||
|  |       </EdgeLabelRenderer> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Edge.propTypes = { | ||||||
|  |   sourceX: PropTypes.number.isRequired, | ||||||
|  |   sourceY: PropTypes.number.isRequired, | ||||||
|  |   targetX: PropTypes.number.isRequired, | ||||||
|  |   targetY: PropTypes.number.isRequired, | ||||||
|  |   source: PropTypes.string.isRequired, | ||||||
|  |   data: PropTypes.shape({ | ||||||
|  |     laidOut: PropTypes.bool, | ||||||
|  |   }).isRequired, | ||||||
|  | }; | ||||||
							
								
								
									
										277
									
								
								packages/web/src/components/EditorNew/EditorNew.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								packages/web/src/components/EditorNew/EditorNew.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | |||||||
|  | import { useEffect, useCallback, createContext, useRef } from 'react'; | ||||||
|  | import { useMutation } from '@apollo/client'; | ||||||
|  | import { useQueryClient } from '@tanstack/react-query'; | ||||||
|  | import { FlowPropType } from 'propTypes/propTypes'; | ||||||
|  | import ReactFlow, { useNodesState, useEdgesState } from 'reactflow'; | ||||||
|  | import 'reactflow/dist/style.css'; | ||||||
|  | import { UPDATE_STEP } from 'graphql/mutations/update-step'; | ||||||
|  | import { CREATE_STEP } from 'graphql/mutations/create-step'; | ||||||
|  |  | ||||||
|  | import { useAutoLayout } from './useAutoLayout'; | ||||||
|  | import { useScrollBoundaries } from './useScrollBoundaries'; | ||||||
|  | import FlowStepNode from './FlowStepNode/FlowStepNode'; | ||||||
|  | import Edge from './Edge/Edge'; | ||||||
|  | import InvisibleNode from './InvisibleNode/InvisibleNode'; | ||||||
|  | import { EditorWrapper } from './style'; | ||||||
|  | import { | ||||||
|  |   generateEdgeId, | ||||||
|  |   generateInitialEdges, | ||||||
|  |   generateInitialNodes, | ||||||
|  |   updatedCollapsedNodes, | ||||||
|  | } from './utils'; | ||||||
|  | import { EDGE_TYPES, INVISIBLE_NODE_ID, NODE_TYPES } from './constants'; | ||||||
|  |  | ||||||
|  | export const EdgesContext = createContext(); | ||||||
|  | export const NodesContext = createContext(); | ||||||
|  |  | ||||||
|  | const nodeTypes = { | ||||||
|  |   [NODE_TYPES.FLOW_STEP]: FlowStepNode, | ||||||
|  |   [NODE_TYPES.INVISIBLE]: InvisibleNode, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const edgeTypes = { | ||||||
|  |   [EDGE_TYPES.ADD_NODE_EDGE]: Edge, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const EditorNew = ({ flow }) => { | ||||||
|  |   const [updateStep] = useMutation(UPDATE_STEP); | ||||||
|  |   const queryClient = useQueryClient(); | ||||||
|  |   const [createStep, { loading: stepCreationInProgress }] = | ||||||
|  |     useMutation(CREATE_STEP); | ||||||
|  |  | ||||||
|  |   const [nodes, setNodes, onNodesChange] = useNodesState( | ||||||
|  |     generateInitialNodes(flow), | ||||||
|  |   ); | ||||||
|  |   const [edges, setEdges, onEdgesChange] = useEdgesState( | ||||||
|  |     generateInitialEdges(flow), | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   useAutoLayout(); | ||||||
|  |   useScrollBoundaries(); | ||||||
|  |  | ||||||
|  |   const createdStepIdRef = useRef(null); | ||||||
|  |  | ||||||
|  |   const openNextStep = useCallback( | ||||||
|  |     (currentStepId) => { | ||||||
|  |       setNodes((nodes) => { | ||||||
|  |         const currentStepIndex = nodes.findIndex( | ||||||
|  |           (node) => node.id === currentStepId, | ||||||
|  |         ); | ||||||
|  |         if (currentStepIndex >= 0) { | ||||||
|  |           const nextStep = nodes[currentStepIndex + 1]; | ||||||
|  |           return updatedCollapsedNodes(nodes, nextStep.id); | ||||||
|  |         } | ||||||
|  |         return nodes; | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     [setNodes], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const onStepClose = useCallback(() => { | ||||||
|  |     setNodes((nodes) => updatedCollapsedNodes(nodes)); | ||||||
|  |   }, [setNodes]); | ||||||
|  |  | ||||||
|  |   const onStepOpen = useCallback( | ||||||
|  |     (stepId) => { | ||||||
|  |       setNodes((nodes) => updatedCollapsedNodes(nodes, stepId)); | ||||||
|  |     }, | ||||||
|  |     [setNodes], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const onStepChange = useCallback( | ||||||
|  |     async (step) => { | ||||||
|  |       const mutationInput = { | ||||||
|  |         id: step.id, | ||||||
|  |         key: step.key, | ||||||
|  |         parameters: step.parameters, | ||||||
|  |         connection: { | ||||||
|  |           id: step.connection?.id, | ||||||
|  |         }, | ||||||
|  |         flow: { | ||||||
|  |           id: flow.id, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       if (step.appKey) { | ||||||
|  |         mutationInput.appKey = step.appKey; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       await updateStep({ | ||||||
|  |         variables: { input: mutationInput }, | ||||||
|  |       }); | ||||||
|  |       await queryClient.invalidateQueries({ | ||||||
|  |         queryKey: ['steps', step.id, 'connection'], | ||||||
|  |       }); | ||||||
|  |       await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] }); | ||||||
|  |     }, | ||||||
|  |     [flow.id, updateStep, queryClient], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const onAddStep = useCallback( | ||||||
|  |     async (previousStepId) => { | ||||||
|  |       const mutationInput = { | ||||||
|  |         previousStep: { | ||||||
|  |           id: previousStepId, | ||||||
|  |         }, | ||||||
|  |         flow: { | ||||||
|  |           id: flow.id, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const { | ||||||
|  |         data: { createStep: createdStep }, | ||||||
|  |       } = await createStep({ | ||||||
|  |         variables: { input: mutationInput }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const createdStepId = createdStep.id; | ||||||
|  |       await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] }); | ||||||
|  |       createdStepIdRef.current = createdStepId; | ||||||
|  |     }, | ||||||
|  |     [flow.id, createStep, queryClient], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (flow.steps.length + 1 !== nodes.length) { | ||||||
|  |       setNodes((nodes) => { | ||||||
|  |         const newNodes = flow.steps.map((step) => { | ||||||
|  |           const createdStepId = createdStepIdRef.current; | ||||||
|  |           const prevNode = nodes.find(({ id }) => id === step.id); | ||||||
|  |           if (prevNode) { | ||||||
|  |             return { | ||||||
|  |               ...prevNode, | ||||||
|  |               zIndex: createdStepId ? 0 : prevNode.zIndex, | ||||||
|  |               data: { | ||||||
|  |                 ...prevNode.data, | ||||||
|  |                 collapsed: createdStepId ? true : prevNode.data.collapsed, | ||||||
|  |               }, | ||||||
|  |             }; | ||||||
|  |           } else { | ||||||
|  |             return { | ||||||
|  |               id: step.id, | ||||||
|  |               type: NODE_TYPES.FLOW_STEP, | ||||||
|  |               position: { | ||||||
|  |                 x: 0, | ||||||
|  |                 y: 0, | ||||||
|  |               }, | ||||||
|  |               zIndex: 1, | ||||||
|  |               data: { | ||||||
|  |                 collapsed: false, | ||||||
|  |                 laidOut: false, | ||||||
|  |               }, | ||||||
|  |             }; | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const prevInvisible = nodes.find(({ id }) => id === INVISIBLE_NODE_ID); | ||||||
|  |         return [ | ||||||
|  |           ...newNodes, | ||||||
|  |           { | ||||||
|  |             id: INVISIBLE_NODE_ID, | ||||||
|  |             type: NODE_TYPES.INVISIBLE, | ||||||
|  |             position: { | ||||||
|  |               x: prevInvisible?.position.x || 0, | ||||||
|  |               y: prevInvisible?.position.y || 0, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         ]; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       setEdges((edges) => { | ||||||
|  |         const newEdges = flow.steps | ||||||
|  |           .map((step, i) => { | ||||||
|  |             const sourceId = step.id; | ||||||
|  |             const targetId = flow.steps[i + 1]?.id; | ||||||
|  |             const edge = edges?.find( | ||||||
|  |               (edge) => edge.id === generateEdgeId(sourceId, targetId), | ||||||
|  |             ); | ||||||
|  |             if (targetId) { | ||||||
|  |               return { | ||||||
|  |                 id: generateEdgeId(sourceId, targetId), | ||||||
|  |                 source: sourceId, | ||||||
|  |                 target: targetId, | ||||||
|  |                 type: 'addNodeEdge', | ||||||
|  |                 data: { | ||||||
|  |                   laidOut: edge ? edge?.data.laidOut : false, | ||||||
|  |                 }, | ||||||
|  |               }; | ||||||
|  |             } | ||||||
|  |             return null; | ||||||
|  |           }) | ||||||
|  |           .filter((edge) => !!edge); | ||||||
|  |  | ||||||
|  |         const lastStep = flow.steps[flow.steps.length - 1]; | ||||||
|  |         const lastEdge = edges[edges.length - 1]; | ||||||
|  |  | ||||||
|  |         return lastStep | ||||||
|  |           ? [ | ||||||
|  |               ...newEdges, | ||||||
|  |               { | ||||||
|  |                 id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID), | ||||||
|  |                 source: lastStep.id, | ||||||
|  |                 target: INVISIBLE_NODE_ID, | ||||||
|  |                 type: 'addNodeEdge', | ||||||
|  |                 data: { | ||||||
|  |                   laidOut: | ||||||
|  |                     lastEdge?.id === | ||||||
|  |                     generateEdgeId(lastStep.id, INVISIBLE_NODE_ID) | ||||||
|  |                       ? lastEdge?.data.laidOut | ||||||
|  |                       : false, | ||||||
|  |                 }, | ||||||
|  |               }, | ||||||
|  |             ] | ||||||
|  |           : newEdges; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (createdStepIdRef.current) { | ||||||
|  |         createdStepIdRef.current = null; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, [flow.steps]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <NodesContext.Provider | ||||||
|  |       value={{ | ||||||
|  |         openNextStep, | ||||||
|  |         onStepOpen, | ||||||
|  |         onStepClose, | ||||||
|  |         onStepChange, | ||||||
|  |         flowId: flow.id, | ||||||
|  |         steps: flow.steps, | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <EdgesContext.Provider | ||||||
|  |         value={{ | ||||||
|  |           stepCreationInProgress, | ||||||
|  |           onAddStep, | ||||||
|  |           flowActive: flow.active, | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         <EditorWrapper direction="column"> | ||||||
|  |           <ReactFlow | ||||||
|  |             nodes={nodes} | ||||||
|  |             edges={edges} | ||||||
|  |             onNodesChange={onNodesChange} | ||||||
|  |             onEdgesChange={onEdgesChange} | ||||||
|  |             nodeTypes={nodeTypes} | ||||||
|  |             edgeTypes={edgeTypes} | ||||||
|  |             panOnScroll | ||||||
|  |             panOnScrollMode="vertical" | ||||||
|  |             panOnDrag={false} | ||||||
|  |             zoomOnScroll={false} | ||||||
|  |             zoomOnPinch={false} | ||||||
|  |             zoomOnDoubleClick={false} | ||||||
|  |             panActivationKeyCode={null} | ||||||
|  |             proOptions={{ hideAttribution: true }} | ||||||
|  |           /> | ||||||
|  |         </EditorWrapper> | ||||||
|  |       </EdgesContext.Provider> | ||||||
|  |     </NodesContext.Provider> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | EditorNew.propTypes = { | ||||||
|  |   flow: FlowPropType.isRequired, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default EditorNew; | ||||||
| @@ -0,0 +1,60 @@ | |||||||
|  | import { Handle, Position } from 'reactflow'; | ||||||
|  | import PropTypes from 'prop-types'; | ||||||
|  |  | ||||||
|  | import FlowStep from 'components/FlowStep'; | ||||||
|  |  | ||||||
|  | import { NodeWrapper, NodeInnerWrapper } from './style.js'; | ||||||
|  | import { useContext } from 'react'; | ||||||
|  | import { NodesContext } from '../EditorNew.jsx'; | ||||||
|  |  | ||||||
|  | function FlowStepNode({ data: { collapsed, laidOut }, id }) { | ||||||
|  |   const { openNextStep, onStepOpen, onStepClose, onStepChange, flowId, steps } = | ||||||
|  |     useContext(NodesContext); | ||||||
|  |  | ||||||
|  |   const step = steps.find(({ id: stepId }) => stepId === id); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <NodeWrapper | ||||||
|  |       className="nodrag" | ||||||
|  |       sx={{ | ||||||
|  |         visibility: laidOut ? 'visible' : 'hidden', | ||||||
|  |       }} | ||||||
|  |     > | ||||||
|  |       <NodeInnerWrapper> | ||||||
|  |         <Handle | ||||||
|  |           type="target" | ||||||
|  |           position={Position.Top} | ||||||
|  |           isConnectable={false} | ||||||
|  |           style={{ visibility: 'hidden' }} | ||||||
|  |         /> | ||||||
|  |         {step && ( | ||||||
|  |           <FlowStep | ||||||
|  |             step={step} | ||||||
|  |             collapsed={collapsed} | ||||||
|  |             onOpen={() => onStepOpen(step.id)} | ||||||
|  |             onClose={onStepClose} | ||||||
|  |             onChange={onStepChange} | ||||||
|  |             flowId={flowId} | ||||||
|  |             onContinue={() => openNextStep(step.id)} | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|  |         <Handle | ||||||
|  |           type="source" | ||||||
|  |           position={Position.Bottom} | ||||||
|  |           isConnectable={false} | ||||||
|  |           style={{ visibility: 'hidden' }} | ||||||
|  |         /> | ||||||
|  |       </NodeInnerWrapper> | ||||||
|  |     </NodeWrapper> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | FlowStepNode.propTypes = { | ||||||
|  |   id: PropTypes.string, | ||||||
|  |   data: PropTypes.shape({ | ||||||
|  |     collapsed: PropTypes.bool.isRequired, | ||||||
|  |     laidOut: PropTypes.bool.isRequired, | ||||||
|  |   }).isRequired, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default FlowStepNode; | ||||||
							
								
								
									
										14
									
								
								packages/web/src/components/EditorNew/FlowStepNode/style.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/web/src/components/EditorNew/FlowStepNode/style.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import { styled } from '@mui/material/styles'; | ||||||
|  | import { Box } from '@mui/material'; | ||||||
|  |  | ||||||
|  | export const NodeWrapper = styled(Box)(({ theme }) => ({ | ||||||
|  |   width: '100vw', | ||||||
|  |   display: 'flex', | ||||||
|  |   justifyContent: 'center', | ||||||
|  |   padding: theme.spacing(0, 2.5), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | export const NodeInnerWrapper = styled(Box)(({ theme }) => ({ | ||||||
|  |   maxWidth: 900, | ||||||
|  |   flex: 1, | ||||||
|  | })); | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | import { Handle, Position } from 'reactflow'; | ||||||
|  | import { Box } from '@mui/material'; | ||||||
|  |  | ||||||
|  | // This node is used for adding an edge with add node button after the last flow step node | ||||||
|  | function InvisibleNode() { | ||||||
|  |   return ( | ||||||
|  |     <Box | ||||||
|  |       maxWidth={900} | ||||||
|  |       width="100vw" | ||||||
|  |       className="nodrag" | ||||||
|  |       sx={{ visibility: 'hidden' }} | ||||||
|  |     > | ||||||
|  |       <Handle type="target" position={Position.Top} isConnectable={false} /> | ||||||
|  |       Invisible node | ||||||
|  |     </Box> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default InvisibleNode; | ||||||
							
								
								
									
										10
									
								
								packages/web/src/components/EditorNew/constants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								packages/web/src/components/EditorNew/constants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | export const INVISIBLE_NODE_ID = 'invisible-node'; | ||||||
|  |  | ||||||
|  | export const NODE_TYPES = { | ||||||
|  |   FLOW_STEP: 'flowStep', | ||||||
|  |   INVISIBLE: 'invisible', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const EDGE_TYPES = { | ||||||
|  |   ADD_NODE_EDGE: 'addNodeEdge', | ||||||
|  | }; | ||||||
							
								
								
									
										13
									
								
								packages/web/src/components/EditorNew/style.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/web/src/components/EditorNew/style.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import { Stack } from '@mui/material'; | ||||||
|  | import { styled } from '@mui/material/styles'; | ||||||
|  |  | ||||||
|  | export const EditorWrapper = styled(Stack)(({ theme }) => ({ | ||||||
|  |   flexGrow: 1, | ||||||
|  |   '& > div': { | ||||||
|  |     flexGrow: 1, | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   '& .react-flow__pane, & .react-flow__node': { | ||||||
|  |     cursor: 'auto !important', | ||||||
|  |   }, | ||||||
|  | })); | ||||||
							
								
								
									
										69
									
								
								packages/web/src/components/EditorNew/useAutoLayout.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								packages/web/src/components/EditorNew/useAutoLayout.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | import { useCallback, useEffect } from 'react'; | ||||||
|  | import Dagre from '@dagrejs/dagre'; | ||||||
|  | import { usePrevious } from 'hooks/usePrevious'; | ||||||
|  | import { isEqual } from 'lodash'; | ||||||
|  | import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow'; | ||||||
|  |  | ||||||
|  | const getLaidOutElements = (nodes, edges) => { | ||||||
|  |   const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); | ||||||
|  |   graph.setGraph({ | ||||||
|  |     rankdir: 'TB', | ||||||
|  |     marginy: 60, | ||||||
|  |     ranksep: 64, | ||||||
|  |   }); | ||||||
|  |   edges.forEach((edge) => graph.setEdge(edge.source, edge.target)); | ||||||
|  |   nodes.forEach((node) => graph.setNode(node.id, node)); | ||||||
|  |  | ||||||
|  |   Dagre.layout(graph); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     nodes: nodes.map((node) => { | ||||||
|  |       const { x, y, width, height } = graph.node(node.id); | ||||||
|  |       return { | ||||||
|  |         ...node, | ||||||
|  |         position: { x: x - width / 2, y: y - height / 2 }, | ||||||
|  |       }; | ||||||
|  |     }), | ||||||
|  |     edges, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const useAutoLayout = () => { | ||||||
|  |   const nodes = useNodes(); | ||||||
|  |   const prevNodes = usePrevious(nodes); | ||||||
|  |   const nodesInitialized = useNodesInitialized(); | ||||||
|  |   const { getEdges, setNodes, setEdges } = useReactFlow(); | ||||||
|  |  | ||||||
|  |   const onLayout = useCallback( | ||||||
|  |     (nodes, edges) => { | ||||||
|  |       const laidOutElements = getLaidOutElements(nodes, edges); | ||||||
|  |  | ||||||
|  |       setNodes([ | ||||||
|  |         ...laidOutElements.nodes.map((node) => ({ | ||||||
|  |           ...node, | ||||||
|  |           data: { ...node.data, laidOut: true }, | ||||||
|  |         })), | ||||||
|  |       ]); | ||||||
|  |       setEdges([ | ||||||
|  |         ...laidOutElements.edges.map((edge) => ({ | ||||||
|  |           ...edge, | ||||||
|  |           data: { ...edge.data, laidOut: true }, | ||||||
|  |         })), | ||||||
|  |       ]); | ||||||
|  |     }, | ||||||
|  |     [setEdges, setNodes], | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const shouldAutoLayout = | ||||||
|  |       nodesInitialized && | ||||||
|  |       !isEqual( | ||||||
|  |         nodes.map(({ width, height }) => ({ width, height })), | ||||||
|  |         prevNodes.map(({ width, height }) => ({ width, height })), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     if (shouldAutoLayout) { | ||||||
|  |       onLayout(nodes, getEdges()); | ||||||
|  |     } | ||||||
|  |   }, [nodes]); | ||||||
|  | }; | ||||||
							
								
								
									
										13
									
								
								packages/web/src/components/EditorNew/useScrollBoundaries.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								packages/web/src/components/EditorNew/useScrollBoundaries.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import { useEffect } from 'react'; | ||||||
|  | import { useViewport, useReactFlow } from 'reactflow'; | ||||||
|  |  | ||||||
|  | export const useScrollBoundaries = () => { | ||||||
|  |   const { setViewport } = useReactFlow(); | ||||||
|  |   const { x, y, zoom } = useViewport(); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (y > 0) { | ||||||
|  |       setViewport({ x, y: 0, zoom }); | ||||||
|  |     } | ||||||
|  |   }, [y]); | ||||||
|  | }; | ||||||
							
								
								
									
										88
									
								
								packages/web/src/components/EditorNew/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								packages/web/src/components/EditorNew/utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | import { INVISIBLE_NODE_ID, NODE_TYPES } from './constants'; | ||||||
|  |  | ||||||
|  | export const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`; | ||||||
|  |  | ||||||
|  | export const updatedCollapsedNodes = (nodes, openStepId) => { | ||||||
|  |   return nodes.map((node) => { | ||||||
|  |     if (node.type !== NODE_TYPES.FLOW_STEP) { | ||||||
|  |       return node; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const collapsed = node.id !== openStepId; | ||||||
|  |     return { | ||||||
|  |       ...node, | ||||||
|  |       zIndex: collapsed ? 0 : 1, | ||||||
|  |       data: { ...node.data, collapsed }, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const generateInitialNodes = (flow) => { | ||||||
|  |   const newNodes = flow.steps.map((step, index) => { | ||||||
|  |     const collapsed = index !== 0; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       id: step.id, | ||||||
|  |       type: NODE_TYPES.FLOW_STEP, | ||||||
|  |       position: { | ||||||
|  |         x: 0, | ||||||
|  |         y: 0, | ||||||
|  |       }, | ||||||
|  |       zIndex: collapsed ? 0 : 1, | ||||||
|  |       data: { | ||||||
|  |         collapsed, | ||||||
|  |         laidOut: false, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return [ | ||||||
|  |     ...newNodes, | ||||||
|  |     { | ||||||
|  |       id: INVISIBLE_NODE_ID, | ||||||
|  |       type: NODE_TYPES.INVISIBLE, | ||||||
|  |       position: { | ||||||
|  |         x: 0, | ||||||
|  |         y: 0, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const generateInitialEdges = (flow) => { | ||||||
|  |   const newEdges = flow.steps | ||||||
|  |     .map((step, i) => { | ||||||
|  |       const sourceId = step.id; | ||||||
|  |       const targetId = flow.steps[i + 1]?.id; | ||||||
|  |       if (targetId) { | ||||||
|  |         return { | ||||||
|  |           id: generateEdgeId(sourceId, targetId), | ||||||
|  |           source: sourceId, | ||||||
|  |           target: targetId, | ||||||
|  |           type: 'addNodeEdge', | ||||||
|  |           data: { | ||||||
|  |             laidOut: false, | ||||||
|  |           }, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     }) | ||||||
|  |     .filter((edge) => !!edge); | ||||||
|  |  | ||||||
|  |   const lastStep = flow.steps[flow.steps.length - 1]; | ||||||
|  |  | ||||||
|  |   return lastStep | ||||||
|  |     ? [ | ||||||
|  |         ...newEdges, | ||||||
|  |         { | ||||||
|  |           id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID), | ||||||
|  |           source: lastStep.id, | ||||||
|  |           target: INVISIBLE_NODE_ID, | ||||||
|  |           type: 'addNodeEdge', | ||||||
|  |           data: { | ||||||
|  |             laidOut: false, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       ] | ||||||
|  |     : newEdges; | ||||||
|  | }; | ||||||
| @@ -28,9 +28,12 @@ function ContextMenu(props) { | |||||||
|       variables: { input: { id: flowId } }, |       variables: { input: { id: flowId } }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     if (appKey) { | ||||||
|       await queryClient.invalidateQueries({ |       await queryClient.invalidateQueries({ | ||||||
|         queryKey: ['apps', appKey, 'flows'], |         queryKey: ['apps', appKey, 'flows'], | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), { |     enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), { | ||||||
|       variant: 'success', |       variant: 'success', | ||||||
|       SnackbarProps: { |       SnackbarProps: { | ||||||
| @@ -56,9 +59,12 @@ function ContextMenu(props) { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     if (appKey) { | ||||||
|       await queryClient.invalidateQueries({ |       await queryClient.invalidateQueries({ | ||||||
|         queryKey: ['apps', appKey, 'flows'], |         queryKey: ['apps', appKey, 'flows'], | ||||||
|       }); |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { |     enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { | ||||||
|       variant: 'success', |       variant: 'success', | ||||||
|     }); |     }); | ||||||
| @@ -110,7 +116,7 @@ ContextMenu.propTypes = { | |||||||
|   ]).isRequired, |   ]).isRequired, | ||||||
|   onDeleteFlow: PropTypes.func, |   onDeleteFlow: PropTypes.func, | ||||||
|   onDuplicateFlow: PropTypes.func, |   onDuplicateFlow: PropTypes.func, | ||||||
|   appKey: PropTypes.string.isRequired, |   appKey: PropTypes.string, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default ContextMenu; | export default ContextMenu; | ||||||
|   | |||||||
| @@ -38,20 +38,24 @@ function FlowRow(props) { | |||||||
|   const contextButtonRef = React.useRef(null); |   const contextButtonRef = React.useRef(null); | ||||||
|   const [anchorEl, setAnchorEl] = React.useState(null); |   const [anchorEl, setAnchorEl] = React.useState(null); | ||||||
|   const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props; |   const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props; | ||||||
|  |  | ||||||
|   const handleClose = () => { |   const handleClose = () => { | ||||||
|     setAnchorEl(null); |     setAnchorEl(null); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const onContextMenuClick = (event) => { |   const onContextMenuClick = (event) => { | ||||||
|     event.preventDefault(); |     event.preventDefault(); | ||||||
|     event.stopPropagation(); |     event.stopPropagation(); | ||||||
|     event.nativeEvent.stopImmediatePropagation(); |     event.nativeEvent.stopImmediatePropagation(); | ||||||
|     setAnchorEl(contextButtonRef.current); |     setAnchorEl(contextButtonRef.current); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10)); |   const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10)); | ||||||
|   const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10)); |   const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10)); | ||||||
|   const isUpdated = updatedAt > createdAt; |   const isUpdated = updatedAt > createdAt; | ||||||
|   const relativeCreatedAt = createdAt.toRelative(); |   const relativeCreatedAt = createdAt.toRelative(); | ||||||
|   const relativeUpdatedAt = updatedAt.toRelative(); |   const relativeUpdatedAt = updatedAt.toRelative(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Card sx={{ mb: 1 }} data-test="flow-row"> |       <Card sx={{ mb: 1 }} data-test="flow-row"> | ||||||
| @@ -127,7 +131,7 @@ FlowRow.propTypes = { | |||||||
|   flow: FlowPropType.isRequired, |   flow: FlowPropType.isRequired, | ||||||
|   onDeleteFlow: PropTypes.func, |   onDeleteFlow: PropTypes.func, | ||||||
|   onDuplicateFlow: PropTypes.func, |   onDuplicateFlow: PropTypes.func, | ||||||
|   appKey: PropTypes.string.isRequired, |   appKey: PropTypes.string, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default FlowRow; | export default FlowRow; | ||||||
|   | |||||||
| @@ -11,9 +11,6 @@ import IconButton from '@mui/material/IconButton'; | |||||||
| import ErrorIcon from '@mui/icons-material/Error'; | import ErrorIcon from '@mui/icons-material/Error'; | ||||||
| import CircularProgress from '@mui/material/CircularProgress'; | import CircularProgress from '@mui/material/CircularProgress'; | ||||||
| import CheckCircleIcon from '@mui/icons-material/CheckCircle'; | import CheckCircleIcon from '@mui/icons-material/CheckCircle'; | ||||||
| import { yupResolver } from '@hookform/resolvers/yup'; |  | ||||||
| import * as yup from 'yup'; |  | ||||||
|  |  | ||||||
| import { EditorContext } from 'contexts/Editor'; | import { EditorContext } from 'contexts/Editor'; | ||||||
| import { StepExecutionsProvider } from 'contexts/StepExecutions'; | import { StepExecutionsProvider } from 'contexts/StepExecutions'; | ||||||
| import TestSubstep from 'components/TestSubstep'; | import TestSubstep from 'components/TestSubstep'; | ||||||
| @@ -33,77 +30,18 @@ import { | |||||||
|   Header, |   Header, | ||||||
|   Wrapper, |   Wrapper, | ||||||
| } from './style'; | } from './style'; | ||||||
| import isEmpty from 'helpers/isEmpty'; |  | ||||||
| import { StepPropType } from 'propTypes/propTypes'; | import { StepPropType } from 'propTypes/propTypes'; | ||||||
| import useTriggers from 'hooks/useTriggers'; | import useTriggers from 'hooks/useTriggers'; | ||||||
| import useActions from 'hooks/useActions'; | import useActions from 'hooks/useActions'; | ||||||
| import useTriggerSubsteps from 'hooks/useTriggerSubsteps'; | import useTriggerSubsteps from 'hooks/useTriggerSubsteps'; | ||||||
| import useActionSubsteps from 'hooks/useActionSubsteps'; | import useActionSubsteps from 'hooks/useActionSubsteps'; | ||||||
| import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions'; | import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions'; | ||||||
|  | import { validationSchemaResolver } from './validation'; | ||||||
|  | import { isEqual } from 'lodash'; | ||||||
|  |  | ||||||
| const validIcon = <CheckCircleIcon color="success" />; | const validIcon = <CheckCircleIcon color="success" />; | ||||||
| const errorIcon = <ErrorIcon color="error" />; | const errorIcon = <ErrorIcon color="error" />; | ||||||
|  |  | ||||||
| function generateValidationSchema(substeps) { |  | ||||||
|   const fieldValidations = substeps?.reduce( |  | ||||||
|     (allValidations, { arguments: args }) => { |  | ||||||
|       if (!args || !Array.isArray(args)) return allValidations; |  | ||||||
|       const substepArgumentValidations = {}; |  | ||||||
|       for (const arg of args) { |  | ||||||
|         const { key, required } = arg; |  | ||||||
|         // base validation for the field if not exists |  | ||||||
|         if (!substepArgumentValidations[key]) { |  | ||||||
|           substepArgumentValidations[key] = yup.mixed(); |  | ||||||
|         } |  | ||||||
|         if ( |  | ||||||
|           typeof substepArgumentValidations[key] === 'object' && |  | ||||||
|           (arg.type === 'string' || arg.type === 'dropdown') |  | ||||||
|         ) { |  | ||||||
|           // if the field is required, add the required validation |  | ||||||
|           if (required) { |  | ||||||
|             substepArgumentValidations[key] = substepArgumentValidations[key] |  | ||||||
|               .required(`${key} is required.`) |  | ||||||
|               .test( |  | ||||||
|                 'empty-check', |  | ||||||
|                 `${key} must be not empty`, |  | ||||||
|                 (value) => !isEmpty(value), |  | ||||||
|               ); |  | ||||||
|           } |  | ||||||
|           // if the field depends on another field, add the dependsOn required validation |  | ||||||
|           if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) { |  | ||||||
|             for (const dependsOnKey of arg.dependsOn) { |  | ||||||
|               const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`; |  | ||||||
|               // TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported. |  | ||||||
|               // So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed. |  | ||||||
|               substepArgumentValidations[key] = substepArgumentValidations[ |  | ||||||
|                 key |  | ||||||
|               ].when(`${dependsOnKey.replace('parameters.', '')}`, { |  | ||||||
|                 is: (value) => Boolean(value) === false, |  | ||||||
|                 then: (schema) => |  | ||||||
|                   schema |  | ||||||
|                     .notOneOf([''], missingDependencyValueMessage) |  | ||||||
|                     .required(missingDependencyValueMessage), |  | ||||||
|               }); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return { |  | ||||||
|         ...allValidations, |  | ||||||
|         ...substepArgumentValidations, |  | ||||||
|       }; |  | ||||||
|     }, |  | ||||||
|     {}, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const validationSchema = yup.object({ |  | ||||||
|     parameters: yup.object(fieldValidations), |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return yupResolver(validationSchema); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function FlowStep(props) { | function FlowStep(props) { | ||||||
|   const { collapsed, onChange, onContinue, flowId } = props; |   const { collapsed, onChange, onContinue, flowId } = props; | ||||||
|   const editorContext = React.useContext(EditorContext); |   const editorContext = React.useContext(EditorContext); | ||||||
| @@ -114,6 +52,10 @@ function FlowStep(props) { | |||||||
|   const isAction = step.type === 'action'; |   const isAction = step.type === 'action'; | ||||||
|   const formatMessage = useFormatMessage(); |   const formatMessage = useFormatMessage(); | ||||||
|   const [currentSubstep, setCurrentSubstep] = React.useState(0); |   const [currentSubstep, setCurrentSubstep] = React.useState(0); | ||||||
|  |   const [formResolverContext, setFormResolverContext] = React.useState({ | ||||||
|  |     substeps: [], | ||||||
|  |     additionalFields: {}, | ||||||
|  |   }); | ||||||
|   const useAppsOptions = {}; |   const useAppsOptions = {}; | ||||||
|  |  | ||||||
|   if (isTrigger) { |   if (isTrigger) { | ||||||
| @@ -168,6 +110,12 @@ function FlowStep(props) { | |||||||
|       ? triggerSubstepsData |       ? triggerSubstepsData | ||||||
|       : actionSubstepsData || []; |       : actionSubstepsData || []; | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     if (!isEqual(substeps, formResolverContext.substeps)) { | ||||||
|  |       setFormResolverContext({ substeps, additionalFields: {} }); | ||||||
|  |     } | ||||||
|  |   }, [substeps]); | ||||||
|  |  | ||||||
|   const handleChange = React.useCallback(({ step }) => { |   const handleChange = React.useCallback(({ step }) => { | ||||||
|     onChange(step); |     onChange(step); | ||||||
|   }, []); |   }, []); | ||||||
| @@ -180,11 +128,6 @@ function FlowStep(props) { | |||||||
|     handleChange({ step: val }); |     handleChange({ step: val }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const stepValidationSchema = React.useMemo( |  | ||||||
|     () => generateValidationSchema(substeps), |  | ||||||
|     [substeps], |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   if (!apps?.data) { |   if (!apps?.data) { | ||||||
|     return ( |     return ( | ||||||
|       <CircularProgress |       <CircularProgress | ||||||
| @@ -213,6 +156,15 @@ function FlowStep(props) { | |||||||
|       value !== substepIndex ? substepIndex : null, |       value !== substepIndex ? substepIndex : null, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |   const addAdditionalFieldsValidation = (additionalFields) => { | ||||||
|  |     if (additionalFields) { | ||||||
|  |       setFormResolverContext((prev) => ({ | ||||||
|  |         ...prev, | ||||||
|  |         additionalFields: { ...prev.additionalFields, ...additionalFields }, | ||||||
|  |       })); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   const validationStatusIcon = |   const validationStatusIcon = | ||||||
|     step.status === 'completed' ? validIcon : errorIcon; |     step.status === 'completed' ? validIcon : errorIcon; | ||||||
|  |  | ||||||
| @@ -266,7 +218,8 @@ function FlowStep(props) { | |||||||
|               <Form |               <Form | ||||||
|                 defaultValues={step} |                 defaultValues={step} | ||||||
|                 onSubmit={handleSubmit} |                 onSubmit={handleSubmit} | ||||||
|                 resolver={stepValidationSchema} |                 resolver={validationSchemaResolver} | ||||||
|  |                 context={formResolverContext} | ||||||
|               > |               > | ||||||
|                 <ChooseAppAndEventSubstep |                 <ChooseAppAndEventSubstep | ||||||
|                   expanded={currentSubstep === 0} |                   expanded={currentSubstep === 0} | ||||||
| @@ -330,6 +283,9 @@ function FlowStep(props) { | |||||||
|                             onSubmit={expandNextStep} |                             onSubmit={expandNextStep} | ||||||
|                             onChange={handleChange} |                             onChange={handleChange} | ||||||
|                             step={step} |                             step={step} | ||||||
|  |                             addAdditionalFieldsValidation={ | ||||||
|  |                               addAdditionalFieldsValidation | ||||||
|  |                             } | ||||||
|                           /> |                           /> | ||||||
|                         )} |                         )} | ||||||
|                     </React.Fragment> |                     </React.Fragment> | ||||||
| @@ -360,7 +316,6 @@ function FlowStep(props) { | |||||||
| FlowStep.propTypes = { | FlowStep.propTypes = { | ||||||
|   collapsed: PropTypes.bool, |   collapsed: PropTypes.bool, | ||||||
|   step: StepPropType.isRequired, |   step: StepPropType.isRequired, | ||||||
|   index: PropTypes.number, |  | ||||||
|   onOpen: PropTypes.func, |   onOpen: PropTypes.func, | ||||||
|   onClose: PropTypes.func, |   onClose: PropTypes.func, | ||||||
|   onChange: PropTypes.func.isRequired, |   onChange: PropTypes.func.isRequired, | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								packages/web/src/components/FlowStep/validation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								packages/web/src/components/FlowStep/validation.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | import * as yup from 'yup'; | ||||||
|  | import { yupResolver } from '@hookform/resolvers/yup'; | ||||||
|  | import isEmpty from 'helpers/isEmpty'; | ||||||
|  |  | ||||||
|  | function addRequiredValidation({ required, schema, key }) { | ||||||
|  |   // if the field is required, add the required validation | ||||||
|  |   if (required) { | ||||||
|  |     return schema | ||||||
|  |       .required(`${key} is required.`) | ||||||
|  |       .test( | ||||||
|  |         'empty-check', | ||||||
|  |         `${key} must be not empty`, | ||||||
|  |         (value) => !isEmpty(value), | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  |   return schema; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function addDependsOnValidation({ schema, dependsOn, key, args }) { | ||||||
|  |   // if the field depends on another field, add the dependsOn required validation | ||||||
|  |   if (Array.isArray(dependsOn) && dependsOn.length > 0) { | ||||||
|  |     for (const dependsOnKey of dependsOn) { | ||||||
|  |       const dependsOnKeyShort = dependsOnKey.replace('parameters.', ''); | ||||||
|  |       const dependsOnField = args.find(({ key }) => key === dependsOnKeyShort); | ||||||
|  |  | ||||||
|  |       if (dependsOnField?.required) { | ||||||
|  |         const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`; | ||||||
|  |  | ||||||
|  |         // TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported. | ||||||
|  |         // So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed. | ||||||
|  |         return schema.when(dependsOnKeyShort, { | ||||||
|  |           is: (dependsOnValue) => Boolean(dependsOnValue) === false, | ||||||
|  |           then: (schema) => | ||||||
|  |             schema | ||||||
|  |               .notOneOf([''], missingDependencyValueMessage) | ||||||
|  |               .required(missingDependencyValueMessage), | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return schema; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function validationSchemaResolver(data, context, options) { | ||||||
|  |   const { substeps = [], additionalFields = {} } = context; | ||||||
|  |  | ||||||
|  |   const fieldValidations = [ | ||||||
|  |     ...substeps, | ||||||
|  |     { | ||||||
|  |       arguments: Object.values(additionalFields) | ||||||
|  |         .filter((field) => !!field) | ||||||
|  |         .flat(), | ||||||
|  |     }, | ||||||
|  |   ].reduce((allValidations, { arguments: args }) => { | ||||||
|  |     if (!args || !Array.isArray(args)) return allValidations; | ||||||
|  |  | ||||||
|  |     const substepArgumentValidations = {}; | ||||||
|  |  | ||||||
|  |     for (const arg of args) { | ||||||
|  |       const { key, required } = arg; | ||||||
|  |  | ||||||
|  |       // base validation for the field if not exists | ||||||
|  |       if (!substepArgumentValidations[key]) { | ||||||
|  |         substepArgumentValidations[key] = yup.mixed(); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (arg.type === 'dynamic') { | ||||||
|  |         const fieldsSchema = {}; | ||||||
|  |  | ||||||
|  |         for (const field of arg.fields) { | ||||||
|  |           fieldsSchema[field.key] = yup.mixed(); | ||||||
|  |  | ||||||
|  |           fieldsSchema[field.key] = addRequiredValidation({ | ||||||
|  |             required: field.required, | ||||||
|  |             schema: fieldsSchema[field.key], | ||||||
|  |             key: field.key, | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           fieldsSchema[field.key] = addDependsOnValidation({ | ||||||
|  |             schema: fieldsSchema[field.key], | ||||||
|  |             dependsOn: field.dependsOn, | ||||||
|  |             key: field.key, | ||||||
|  |             args, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         substepArgumentValidations[key] = yup | ||||||
|  |           .array() | ||||||
|  |           .of(yup.object(fieldsSchema)); | ||||||
|  |       } else if ( | ||||||
|  |         typeof substepArgumentValidations[key] === 'object' && | ||||||
|  |         (arg.type === 'string' || arg.type === 'dropdown') | ||||||
|  |       ) { | ||||||
|  |         substepArgumentValidations[key] = addRequiredValidation({ | ||||||
|  |           required, | ||||||
|  |           schema: substepArgumentValidations[key], | ||||||
|  |           key, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         substepArgumentValidations[key] = addDependsOnValidation({ | ||||||
|  |           schema: substepArgumentValidations[key], | ||||||
|  |           dependsOn: arg.dependsOn, | ||||||
|  |           key, | ||||||
|  |           args, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...allValidations, | ||||||
|  |       ...substepArgumentValidations, | ||||||
|  |     }; | ||||||
|  |   }, {}); | ||||||
|  |  | ||||||
|  |   const validationSchema = yup.object({ | ||||||
|  |     parameters: yup.object(fieldValidations), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return yupResolver(validationSchema)(data, context, options); | ||||||
|  | } | ||||||
| @@ -43,7 +43,10 @@ function FlowStepContextMenu(props) { | |||||||
| FlowStepContextMenu.propTypes = { | FlowStepContextMenu.propTypes = { | ||||||
|   stepId: PropTypes.string.isRequired, |   stepId: PropTypes.string.isRequired, | ||||||
|   onClose: PropTypes.func.isRequired, |   onClose: PropTypes.func.isRequired, | ||||||
|   anchorEl: PropTypes.element.isRequired, |   anchorEl: PropTypes.oneOfType([ | ||||||
|  |     PropTypes.func, | ||||||
|  |     PropTypes.shape({ current: PropTypes.instanceOf(Element) }), | ||||||
|  |   ]).isRequired, | ||||||
|   deletable: PropTypes.bool.isRequired, |   deletable: PropTypes.bool.isRequired, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,7 +19,9 @@ function FlowSubstep(props) { | |||||||
|     onCollapse, |     onCollapse, | ||||||
|     onSubmit, |     onSubmit, | ||||||
|     step, |     step, | ||||||
|  |     addAdditionalFieldsValidation, | ||||||
|   } = props; |   } = props; | ||||||
|  |  | ||||||
|   const { name, arguments: args } = substep; |   const { name, arguments: args } = substep; | ||||||
|   const editorContext = React.useContext(EditorContext); |   const editorContext = React.useContext(EditorContext); | ||||||
|   const formContext = useFormContext(); |   const formContext = useFormContext(); | ||||||
| @@ -54,6 +56,7 @@ function FlowSubstep(props) { | |||||||
|                   stepId={step.id} |                   stepId={step.id} | ||||||
|                   disabled={editorContext.readOnly} |                   disabled={editorContext.readOnly} | ||||||
|                   showOptionValue={true} |                   showOptionValue={true} | ||||||
|  |                   addAdditionalFieldsValidation={addAdditionalFieldsValidation} | ||||||
|                 /> |                 /> | ||||||
|               ))} |               ))} | ||||||
|             </Stack> |             </Stack> | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import * as React from 'react'; | import * as React from 'react'; | ||||||
| import { FormProvider, useForm, useWatch } from 'react-hook-form'; | import { FormProvider, useForm, useWatch } from 'react-hook-form'; | ||||||
|  |  | ||||||
| const noop = () => null; | const noop = () => null; | ||||||
|  |  | ||||||
| export default function Form(props) { | export default function Form(props) { | ||||||
|   const { |   const { | ||||||
|     children, |     children, | ||||||
| @@ -9,24 +11,31 @@ export default function Form(props) { | |||||||
|     resolver, |     resolver, | ||||||
|     render, |     render, | ||||||
|     mode = 'all', |     mode = 'all', | ||||||
|  |     context, | ||||||
|     ...formProps |     ...formProps | ||||||
|   } = props; |   } = props; | ||||||
|  |  | ||||||
|   const methods = useForm({ |   const methods = useForm({ | ||||||
|     defaultValues, |     defaultValues, | ||||||
|     reValidateMode: 'onBlur', |     reValidateMode: 'onBlur', | ||||||
|     resolver, |     resolver, | ||||||
|     mode, |     mode, | ||||||
|  |     context, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const form = useWatch({ control: methods.control }); |   const form = useWatch({ control: methods.control }); | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * For fields having `dependsOn` fields, we need to re-validate the form. |    * For fields having `dependsOn` fields, we need to re-validate the form. | ||||||
|    */ |    */ | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     methods.trigger(); |     methods.trigger(); | ||||||
|   }, [methods.trigger, form]); |   }, [methods.trigger, form]); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     methods.reset(defaultValues); |     methods.reset(defaultValues); | ||||||
|   }, [defaultValues]); |   }, [defaultValues]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <FormProvider {...methods}> |     <FormProvider {...methods}> | ||||||
|       <form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}> |       <form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}> | ||||||
|   | |||||||
| @@ -23,7 +23,9 @@ export default function InputCreator(props) { | |||||||
|     disabled, |     disabled, | ||||||
|     showOptionValue, |     showOptionValue, | ||||||
|     shouldUnregister, |     shouldUnregister, | ||||||
|  |     addAdditionalFieldsValidation, | ||||||
|   } = props; |   } = props; | ||||||
|  |  | ||||||
|   const { |   const { | ||||||
|     key: name, |     key: name, | ||||||
|     label, |     label, | ||||||
| @@ -33,6 +35,7 @@ export default function InputCreator(props) { | |||||||
|     description, |     description, | ||||||
|     type, |     type, | ||||||
|   } = schema; |   } = schema; | ||||||
|  |  | ||||||
|   const { data, loading } = useDynamicData(stepId, schema); |   const { data, loading } = useDynamicData(stepId, schema); | ||||||
|   const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } = |   const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } = | ||||||
|     useDynamicFields(stepId, schema); |     useDynamicFields(stepId, schema); | ||||||
| @@ -40,6 +43,10 @@ export default function InputCreator(props) { | |||||||
|  |  | ||||||
|   const computedName = namePrefix ? `${namePrefix}.${name}` : name; |   const computedName = namePrefix ? `${namePrefix}.${name}` : name; | ||||||
|  |  | ||||||
|  |   React.useEffect(() => { | ||||||
|  |     addAdditionalFieldsValidation?.({ [name]: additionalFields }); | ||||||
|  |   }, [additionalFields]); | ||||||
|  |  | ||||||
|   if (type === 'dynamic') { |   if (type === 'dynamic') { | ||||||
|     return ( |     return ( | ||||||
|       <DynamicField |       <DynamicField | ||||||
| @@ -80,6 +87,7 @@ export default function InputCreator(props) { | |||||||
|             disabled={disabled} |             disabled={disabled} | ||||||
|             showOptionValue={showOptionValue} |             showOptionValue={showOptionValue} | ||||||
|             shouldUnregister={shouldUnregister} |             shouldUnregister={shouldUnregister} | ||||||
|  |             componentsProps={{ popper: { className: 'nowheel' } }} | ||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,8 +5,10 @@ import AddCircleIcon from '@mui/icons-material/AddCircle'; | |||||||
| import CardActionArea from '@mui/material/CardActionArea'; | import CardActionArea from '@mui/material/CardActionArea'; | ||||||
| import Typography from '@mui/material/Typography'; | import Typography from '@mui/material/Typography'; | ||||||
| import { CardContent } from './style'; | import { CardContent } from './style'; | ||||||
|  |  | ||||||
| export default function NoResultFound(props) { | export default function NoResultFound(props) { | ||||||
|   const { text, to } = props; |   const { text, to } = props; | ||||||
|  |  | ||||||
|   const ActionAreaLink = React.useMemo( |   const ActionAreaLink = React.useMemo( | ||||||
|     () => |     () => | ||||||
|       React.forwardRef(function InlineLink(linkProps, ref) { |       React.forwardRef(function InlineLink(linkProps, ref) { | ||||||
| @@ -15,12 +17,12 @@ export default function NoResultFound(props) { | |||||||
|       }), |       }), | ||||||
|     [to], |     [to], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card elevation={0}> |     <Card elevation={0}> | ||||||
|       <CardActionArea component={ActionAreaLink} {...props}> |       <CardActionArea component={ActionAreaLink} {...props}> | ||||||
|         <CardContent> |         <CardContent> | ||||||
|           {!!to && <AddCircleIcon color="primary" />} |           {!!to && <AddCircleIcon color="primary" />} | ||||||
|  |  | ||||||
|           <Typography variant="body1">{text}</Typography> |           <Typography variant="body1">{text}</Typography> | ||||||
|         </CardContent> |         </CardContent> | ||||||
|       </CardActionArea> |       </CardActionArea> | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import { StepExecutionsContext } from 'contexts/StepExecutions'; | |||||||
| import Popper from './Popper'; | import Popper from './Popper'; | ||||||
| import { processStepWithExecutions } from './data'; | import { processStepWithExecutions } from './data'; | ||||||
| import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style'; | import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style'; | ||||||
|  |  | ||||||
| const PowerInput = (props) => { | const PowerInput = (props) => { | ||||||
|   const { control } = useFormContext(); |   const { control } = useFormContext(); | ||||||
|   const { |   const { | ||||||
| @@ -31,33 +32,41 @@ const PowerInput = (props) => { | |||||||
|   } = props; |   } = props; | ||||||
|   const priorStepsWithExecutions = React.useContext(StepExecutionsContext); |   const priorStepsWithExecutions = React.useContext(StepExecutionsContext); | ||||||
|   const editorRef = React.useRef(null); |   const editorRef = React.useRef(null); | ||||||
|  |  | ||||||
|   const renderElement = React.useCallback( |   const renderElement = React.useCallback( | ||||||
|     (props) => <Element {...props} />, |     (props) => <Element {...props} />, | ||||||
|     [], |     [], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const [editor] = React.useState(() => customizeEditor(createEditor())); |   const [editor] = React.useState(() => customizeEditor(createEditor())); | ||||||
|  |  | ||||||
|   const [showVariableSuggestions, setShowVariableSuggestions] = |   const [showVariableSuggestions, setShowVariableSuggestions] = | ||||||
|     React.useState(false); |     React.useState(false); | ||||||
|  |  | ||||||
|   const disappearSuggestionsOnShift = (event) => { |   const disappearSuggestionsOnShift = (event) => { | ||||||
|     if (event.code === 'Tab') { |     if (event.code === 'Tab') { | ||||||
|       setShowVariableSuggestions(false); |       setShowVariableSuggestions(false); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const stepsWithVariables = React.useMemo(() => { |   const stepsWithVariables = React.useMemo(() => { | ||||||
|     return processStepWithExecutions(priorStepsWithExecutions); |     return processStepWithExecutions(priorStepsWithExecutions); | ||||||
|   }, [priorStepsWithExecutions]); |   }, [priorStepsWithExecutions]); | ||||||
|  |  | ||||||
|   const handleBlur = React.useCallback( |   const handleBlur = React.useCallback( | ||||||
|     (value) => { |     (value) => { | ||||||
|       onBlur?.(value); |       onBlur?.(value); | ||||||
|     }, |     }, | ||||||
|     [onBlur], |     [onBlur], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const handleVariableSuggestionClick = React.useCallback( |   const handleVariableSuggestionClick = React.useCallback( | ||||||
|     (variable) => { |     (variable) => { | ||||||
|       insertVariable(editor, variable, stepsWithVariables); |       insertVariable(editor, variable, stepsWithVariables); | ||||||
|     }, |     }, | ||||||
|     [stepsWithVariables], |     [stepsWithVariables], | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Controller |     <Controller | ||||||
|       rules={{ required }} |       rules={{ required }} | ||||||
| @@ -127,6 +136,7 @@ const PowerInput = (props) => { | |||||||
|                 anchorEl={editorRef.current} |                 anchorEl={editorRef.current} | ||||||
|                 data={stepsWithVariables} |                 data={stepsWithVariables} | ||||||
|                 onSuggestionClick={handleVariableSuggestionClick} |                 onSuggestionClick={handleVariableSuggestionClick} | ||||||
|  |                 className="nowheel" | ||||||
|               /> |               /> | ||||||
|  |  | ||||||
|               <FormHelperText variant="outlined">{description}</FormHelperText> |               <FormHelperText variant="outlined">{description}</FormHelperText> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query'; | |||||||
| import api from 'helpers/api'; | import api from 'helpers/api'; | ||||||
|  |  | ||||||
| const variableRegExp = /({.*?})/; | const variableRegExp = /({.*?})/; | ||||||
|  |  | ||||||
| // TODO: extract this function to a separate file | // TODO: extract this function to a separate file | ||||||
| function computeArguments(args, getValues) { | function computeArguments(args, getValues) { | ||||||
|   const initialValue = {}; |   const initialValue = {}; | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								packages/web/src/hooks/usePrevious.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								packages/web/src/hooks/usePrevious.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import { useEffect, useRef } from "react"; | ||||||
|  |  | ||||||
|  | export const usePrevious = (value) => { | ||||||
|  |   const ref = useRef(); | ||||||
|  |   useEffect(() => { | ||||||
|  |     ref.current = value; | ||||||
|  |   }); | ||||||
|  |   return ref.current; | ||||||
|  | }; | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| import { createRoot } from 'react-dom/client'; | import { createRoot } from 'react-dom/client'; | ||||||
|  | import { Settings } from 'luxon'; | ||||||
|  |  | ||||||
| import ThemeProvider from 'components/ThemeProvider'; | import ThemeProvider from 'components/ThemeProvider'; | ||||||
| import IntlProvider from 'components/IntlProvider'; | import IntlProvider from 'components/IntlProvider'; | ||||||
| import ApolloProvider from 'components/ApolloProvider'; | import ApolloProvider from 'components/ApolloProvider'; | ||||||
| @@ -10,6 +12,9 @@ import Router from 'components/Router'; | |||||||
| import routes from 'routes'; | import routes from 'routes'; | ||||||
| import reportWebVitals from './reportWebVitals'; | import reportWebVitals from './reportWebVitals'; | ||||||
|  |  | ||||||
|  | // Sets the default locale to English for all luxon DateTime instances created afterwards. | ||||||
|  | Settings.defaultLocale = 'en'; | ||||||
|  |  | ||||||
| const container = document.getElementById('root'); | const container = document.getElementById('root'); | ||||||
| const root = createRoot(container); | const root = createRoot(container); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ import AppIcon from 'components/AppIcon'; | |||||||
| import Container from 'components/Container'; | import Container from 'components/Container'; | ||||||
| import PageTitle from 'components/PageTitle'; | import PageTitle from 'components/PageTitle'; | ||||||
| import useApp from 'hooks/useApp'; | import useApp from 'hooks/useApp'; | ||||||
|  | import Can from 'components/Can'; | ||||||
|  |  | ||||||
| const ReconnectConnection = (props) => { | const ReconnectConnection = (props) => { | ||||||
|   const { application, onClose } = props; |   const { application, onClose } = props; | ||||||
| @@ -92,7 +93,7 @@ export default function Application() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return options; |     return options; | ||||||
|   }, [appKey, appConfig?.data, currentUserAbility]); |   }, [appKey, appConfig?.data, currentUserAbility, formatMessage]); | ||||||
|  |  | ||||||
|   if (loading) return null; |   if (loading) return null; | ||||||
|  |  | ||||||
| @@ -118,6 +119,8 @@ export default function Application() { | |||||||
|                 <Route |                 <Route | ||||||
|                   path={`${URLS.FLOWS}/*`} |                   path={`${URLS.FLOWS}/*`} | ||||||
|                   element={ |                   element={ | ||||||
|  |                     <Can I="create" a="Flow" passThrough> | ||||||
|  |                       {(allowed) => ( | ||||||
|                         <ConditionalIconButton |                         <ConditionalIconButton | ||||||
|                           type="submit" |                           type="submit" | ||||||
|                           variant="contained" |                           variant="contained" | ||||||
| @@ -130,18 +133,23 @@ export default function Application() { | |||||||
|                           )} |                           )} | ||||||
|                           fullWidth |                           fullWidth | ||||||
|                           icon={<AddIcon />} |                           icon={<AddIcon />} | ||||||
|                       disabled={!currentUserAbility.can('create', 'Flow')} |                           disabled={!allowed} | ||||||
|                         > |                         > | ||||||
|                           {formatMessage('app.createFlow')} |                           {formatMessage('app.createFlow')} | ||||||
|                         </ConditionalIconButton> |                         </ConditionalIconButton> | ||||||
|  |                       )} | ||||||
|  |                     </Can> | ||||||
|                   } |                   } | ||||||
|                 /> |                 /> | ||||||
|  |  | ||||||
|                 <Route |                 <Route | ||||||
|                   path={`${URLS.CONNECTIONS}/*`} |                   path={`${URLS.CONNECTIONS}/*`} | ||||||
|                   element={ |                   element={ | ||||||
|  |                     <Can I="create" a="Connection" passThrough> | ||||||
|  |                       {(allowed) => ( | ||||||
|                         <SplitButton |                         <SplitButton | ||||||
|                           disabled={ |                           disabled={ | ||||||
|  |                             !allowed || | ||||||
|                             (appConfig?.data && |                             (appConfig?.data && | ||||||
|                               !appConfig?.data?.canConnect && |                               !appConfig?.data?.canConnect && | ||||||
|                               !appConfig?.data?.canCustomConnect) || |                               !appConfig?.data?.canCustomConnect) || | ||||||
| @@ -149,6 +157,8 @@ export default function Application() { | |||||||
|                           } |                           } | ||||||
|                           options={connectionOptions} |                           options={connectionOptions} | ||||||
|                         /> |                         /> | ||||||
|  |                       )} | ||||||
|  |                     </Can> | ||||||
|                   } |                   } | ||||||
|                 /> |                 /> | ||||||
|               </Routes> |               </Routes> | ||||||
| @@ -169,17 +179,20 @@ export default function Application() { | |||||||
|                     label={formatMessage('app.connections')} |                     label={formatMessage('app.connections')} | ||||||
|                     to={URLS.APP_CONNECTIONS(appKey)} |                     to={URLS.APP_CONNECTIONS(appKey)} | ||||||
|                     value={URLS.APP_CONNECTIONS_PATTERN} |                     value={URLS.APP_CONNECTIONS_PATTERN} | ||||||
|                     disabled={!app.supportsConnections} |                     disabled={ | ||||||
|  |                       !currentUserAbility.can('read', 'Connection') || | ||||||
|  |                       !app.supportsConnections | ||||||
|  |                     } | ||||||
|                     component={Link} |                     component={Link} | ||||||
|                     data-test="connections-tab" |                     data-test="connections-tab" | ||||||
|                   /> |                   /> | ||||||
|  |  | ||||||
|                   <Tab |                   <Tab | ||||||
|                     label={formatMessage('app.flows')} |                     label={formatMessage('app.flows')} | ||||||
|                     to={URLS.APP_FLOWS(appKey)} |                     to={URLS.APP_FLOWS(appKey)} | ||||||
|                     value={URLS.APP_FLOWS_PATTERN} |                     value={URLS.APP_FLOWS_PATTERN} | ||||||
|                     component={Link} |                     component={Link} | ||||||
|                     data-test="flows-tab" |                     data-test="flows-tab" | ||||||
|  |                     disabled={!currentUserAbility.can('read', 'Flow')} | ||||||
|                   /> |                   /> | ||||||
|                 </Tabs> |                 </Tabs> | ||||||
|               </Box> |               </Box> | ||||||
| @@ -187,14 +200,20 @@ export default function Application() { | |||||||
|               <Routes> |               <Routes> | ||||||
|                 <Route |                 <Route | ||||||
|                   path={`${URLS.FLOWS}/*`} |                   path={`${URLS.FLOWS}/*`} | ||||||
|                   element={<AppFlows appKey={appKey} />} |                   element={ | ||||||
|  |                     <Can I="read" a="Flow"> | ||||||
|  |                       <AppFlows appKey={appKey} /> | ||||||
|  |                     </Can> | ||||||
|  |                   } | ||||||
|                 /> |                 /> | ||||||
|  |  | ||||||
|                 <Route |                 <Route | ||||||
|                   path={`${URLS.CONNECTIONS}/*`} |                   path={`${URLS.CONNECTIONS}/*`} | ||||||
|                   element={<AppConnections appKey={appKey} />} |                   element={ | ||||||
|  |                     <Can I="read" a="Connection"> | ||||||
|  |                       <AppConnections appKey={appKey} /> | ||||||
|  |                     </Can> | ||||||
|  |                   } | ||||||
|                 /> |                 /> | ||||||
|  |  | ||||||
|                 <Route |                 <Route | ||||||
|                   path="/" |                   path="/" | ||||||
|                   element={ |                   element={ | ||||||
| @@ -218,17 +237,24 @@ export default function Application() { | |||||||
|         <Route |         <Route | ||||||
|           path="/connections/add" |           path="/connections/add" | ||||||
|           element={ |           element={ | ||||||
|             <AddAppConnection onClose={goToApplicationPage} application={app} /> |             <Can I="create" a="Connection"> | ||||||
|  |               <AddAppConnection | ||||||
|  |                 onClose={goToApplicationPage} | ||||||
|  |                 application={app} | ||||||
|  |               /> | ||||||
|  |             </Can> | ||||||
|           } |           } | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|         <Route |         <Route | ||||||
|           path="/connections/:connectionId/reconnect" |           path="/connections/:connectionId/reconnect" | ||||||
|           element={ |           element={ | ||||||
|  |             <Can I="create" a="Connection"> | ||||||
|               <ReconnectConnection |               <ReconnectConnection | ||||||
|                 application={app} |                 application={app} | ||||||
|                 onClose={goToApplicationPage} |                 onClose={goToApplicationPage} | ||||||
|               /> |               /> | ||||||
|  |             </Can> | ||||||
|           } |           } | ||||||
|         /> |         /> | ||||||
|       </Routes> |       </Routes> | ||||||
|   | |||||||
| @@ -84,11 +84,15 @@ export default function Applications() { | |||||||
|         )} |         )} | ||||||
|  |  | ||||||
|         {!isLoading && !hasApps && ( |         {!isLoading && !hasApps && ( | ||||||
|  |           <Can I="create" a="Connection" passThrough> | ||||||
|  |             {(allowed) => ( | ||||||
|               <NoResultFound |               <NoResultFound | ||||||
|                 text={formatMessage('apps.noConnections')} |                 text={formatMessage('apps.noConnections')} | ||||||
|             to={URLS.NEW_APP_CONNECTION} |                 {...(allowed && { to: URLS.NEW_APP_CONNECTION })} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
|  |           </Can> | ||||||
|  |         )} | ||||||
|  |  | ||||||
|         {!isLoading && |         {!isLoading && | ||||||
|           apps?.map((app) => ( |           apps?.map((app) => ( | ||||||
|   | |||||||
| @@ -7,13 +7,15 @@ import * as URLS from 'config/urls'; | |||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
| import { CREATE_FLOW } from 'graphql/mutations/create-flow'; | import { CREATE_FLOW } from 'graphql/mutations/create-flow'; | ||||||
| import Box from '@mui/material/Box'; | import Box from '@mui/material/Box'; | ||||||
|  |  | ||||||
| export default function CreateFlow() { | export default function CreateFlow() { | ||||||
|   const [searchParams] = useSearchParams(); |   const [searchParams] = useSearchParams(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const formatMessage = useFormatMessage(); |   const formatMessage = useFormatMessage(); | ||||||
|   const [createFlow] = useMutation(CREATE_FLOW); |   const [createFlow, { error }] = useMutation(CREATE_FLOW); | ||||||
|   const appKey = searchParams.get('appKey'); |   const appKey = searchParams.get('appKey'); | ||||||
|   const connectionId = searchParams.get('connectionId'); |   const connectionId = searchParams.get('connectionId'); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     async function initiate() { |     async function initiate() { | ||||||
|       const variables = {}; |       const variables = {}; | ||||||
| @@ -33,6 +35,11 @@ export default function CreateFlow() { | |||||||
|     } |     } | ||||||
|     initiate(); |     initiate(); | ||||||
|   }, [createFlow, navigate, appKey, connectionId]); |   }, [createFlow, navigate, appKey, connectionId]); | ||||||
|  |  | ||||||
|  |   if (error) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Box |     <Box | ||||||
|       sx={{ |       sx={{ | ||||||
| @@ -45,7 +52,6 @@ export default function CreateFlow() { | |||||||
|       }} |       }} | ||||||
|     > |     > | ||||||
|       <CircularProgress size={16} thickness={7.5} /> |       <CircularProgress size={16} thickness={7.5} /> | ||||||
|  |  | ||||||
|       <Typography variant="body2"> |       <Typography variant="body2"> | ||||||
|         {formatMessage('createFlow.creating')} |         {formatMessage('createFlow.creating')} | ||||||
|       </Typography> |       </Typography> | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import Container from 'components/Container'; | |||||||
| import PageTitle from 'components/PageTitle'; | import PageTitle from 'components/PageTitle'; | ||||||
| import SearchInput from 'components/SearchInput'; | import SearchInput from 'components/SearchInput'; | ||||||
| import useFormatMessage from 'hooks/useFormatMessage'; | import useFormatMessage from 'hooks/useFormatMessage'; | ||||||
|  | import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; | ||||||
| import * as URLS from 'config/urls'; | import * as URLS from 'config/urls'; | ||||||
| import useLazyFlows from 'hooks/useLazyFlows'; | import useLazyFlows from 'hooks/useLazyFlows'; | ||||||
|  |  | ||||||
| @@ -26,6 +27,7 @@ export default function Flows() { | |||||||
|   const page = parseInt(searchParams.get('page') || '', 10) || 1; |   const page = parseInt(searchParams.get('page') || '', 10) || 1; | ||||||
|   const [flowName, setFlowName] = React.useState(''); |   const [flowName, setFlowName] = React.useState(''); | ||||||
|   const [isLoading, setIsLoading] = React.useState(false); |   const [isLoading, setIsLoading] = React.useState(false); | ||||||
|  |   const currentUserAbility = useCurrentUserAbility(); | ||||||
|  |  | ||||||
|   const { data, mutate: fetchFlows } = useLazyFlows( |   const { data, mutate: fetchFlows } = useLazyFlows( | ||||||
|     { flowName, page }, |     { flowName, page }, | ||||||
| @@ -124,7 +126,9 @@ export default function Flows() { | |||||||
|         {!isLoading && !hasFlows && ( |         {!isLoading && !hasFlows && ( | ||||||
|           <NoResultFound |           <NoResultFound | ||||||
|             text={formatMessage('flows.noFlows')} |             text={formatMessage('flows.noFlows')} | ||||||
|             to={URLS.CREATE_FLOW} |             {...(currentUserAbility.can('create', 'Flow') && { | ||||||
|  |               to: URLS.CREATE_FLOW, | ||||||
|  |             })} | ||||||
|           /> |           /> | ||||||
|         )} |         )} | ||||||
|         {!isLoading && pageInfo && pageInfo.totalPages > 1 && ( |         {!isLoading && pageInfo && pageInfo.totalPages > 1 && ( | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ services: | |||||||
|     name: automatisch-main |     name: automatisch-main | ||||||
|     env: docker |     env: docker | ||||||
|     dockerfilePath: ./docker/Dockerfile |     dockerfilePath: ./docker/Dockerfile | ||||||
|     dockerContext: ./docker |     dockerContext: . | ||||||
|     repo: https://github.com/automatisch/automatisch |     repo: https://github.com/automatisch/automatisch | ||||||
|     autoDeploy: false |     autoDeploy: false | ||||||
|     envVars: |     envVars: | ||||||
| @@ -47,7 +47,7 @@ services: | |||||||
|     name: automatisch-worker |     name: automatisch-worker | ||||||
|     env: docker |     env: docker | ||||||
|     dockerfilePath: ./docker/Dockerfile |     dockerfilePath: ./docker/Dockerfile | ||||||
|     dockerContext: ./docker |     dockerContext: . | ||||||
|     repo: https://github.com/automatisch/automatisch |     repo: https://github.com/automatisch/automatisch | ||||||
|     autoDeploy: false |     autoDeploy: false | ||||||
|     envVars: |     envVars: | ||||||
|   | |||||||
							
								
								
									
										384
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										384
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1455,6 +1455,18 @@ | |||||||
|     enabled "2.0.x" |     enabled "2.0.x" | ||||||
|     kuler "^2.0.0" |     kuler "^2.0.0" | ||||||
|  |  | ||||||
|  | "@dagrejs/dagre@^1.1.2": | ||||||
|  |   version "1.1.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.2.tgz#5ec339979447091f48d2144deed8c70dfadae374" | ||||||
|  |   integrity sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw== | ||||||
|  |   dependencies: | ||||||
|  |     "@dagrejs/graphlib" "2.2.2" | ||||||
|  |  | ||||||
|  | "@dagrejs/graphlib@2.2.2": | ||||||
|  |   version "2.2.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.2.tgz#74154d5cb880a23b4fae71034a09b4b5aef06feb" | ||||||
|  |   integrity sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg== | ||||||
|  |  | ||||||
| "@docsearch/css@3.2.1", "@docsearch/css@^3.2.1": | "@docsearch/css@3.2.1", "@docsearch/css@^3.2.1": | ||||||
|   version "3.2.1" |   version "3.2.1" | ||||||
|   resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz" |   resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz" | ||||||
| @@ -3333,6 +3345,72 @@ | |||||||
|   resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" |   resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" | ||||||
|   integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== |   integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== | ||||||
|  |  | ||||||
|  | "@reactflow/background@11.3.12": | ||||||
|  |   version "11.3.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.12.tgz#9c9491cce4659bae13074fcdb48ac25664879d3f" | ||||||
|  |   integrity sha512-jBuWVb43JQy5h4WOS7G0PU8voGTEJNA+qDmx8/jyBtrjbasTesLNfQvboTGjnQYYiJco6mw5vrtQItAJDNoIqw== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     classcat "^5.0.3" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
|  | "@reactflow/controls@11.2.12": | ||||||
|  |   version "11.2.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.12.tgz#85e2aa5de17e2af28a5ecf6a75bb9c828a20640b" | ||||||
|  |   integrity sha512-L9F3+avFRShoprdT+5oOijm5gVsz2rqWCXBzOAgD923L1XFGIspdiHLLf8IlPGsT+mfl0GxbptZhaEeEzl1e3g== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     classcat "^5.0.3" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
|  | "@reactflow/core@11.11.2": | ||||||
|  |   version "11.11.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.2.tgz#c62f78297bda9d2e86a12228617ec3f91fbd4b22" | ||||||
|  |   integrity sha512-+GfgyskweL1PsgRSguUwfrT2eDotlFgaKfDLm7x0brdzzPJY2qbCzVetaxedaiJmIli3817iYbILvE9qLKwbRA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3" "^7.4.0" | ||||||
|  |     "@types/d3-drag" "^3.0.1" | ||||||
|  |     "@types/d3-selection" "^3.0.3" | ||||||
|  |     "@types/d3-zoom" "^3.0.1" | ||||||
|  |     classcat "^5.0.3" | ||||||
|  |     d3-drag "^3.0.0" | ||||||
|  |     d3-selection "^3.0.0" | ||||||
|  |     d3-zoom "^3.0.0" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
|  | "@reactflow/minimap@11.7.12": | ||||||
|  |   version "11.7.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.12.tgz#6b2fc671ee17e37ccd3bc038ae8d2121d0ce6291" | ||||||
|  |   integrity sha512-SRDU77c2PCF54PV/MQfkz7VOW46q7V1LZNOQlXAp7dkNyAOI6R+tb9qBUtUJOvILB+TCN6pRfD9fQ+2T99bW3Q== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     "@types/d3-selection" "^3.0.3" | ||||||
|  |     "@types/d3-zoom" "^3.0.1" | ||||||
|  |     classcat "^5.0.3" | ||||||
|  |     d3-selection "^3.0.0" | ||||||
|  |     d3-zoom "^3.0.0" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
|  | "@reactflow/node-resizer@2.2.12": | ||||||
|  |   version "2.2.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.12.tgz#df82a7dfba883afea6a01a9c8210008a1ddba01f" | ||||||
|  |   integrity sha512-6LHJGuI1zHyRrZHw5gGlVLIWnvVxid9WIqw8FMFSg+oF2DuS3pAPwSoZwypy7W22/gDNl9eD1Dcl/OtFtDFQ+w== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     classcat "^5.0.4" | ||||||
|  |     d3-drag "^3.0.0" | ||||||
|  |     d3-selection "^3.0.0" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
|  | "@reactflow/node-toolbar@1.3.12": | ||||||
|  |   version "1.3.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.12.tgz#89e7aa9d34b6213bb5e64c344d4e2e3cb7af3163" | ||||||
|  |   integrity sha512-4kJRvNna/E3y2MZW9/80wTKwkhw4pLJiz3D5eQrD13XcmojSb1rArO9CiwyrI+rMvs5gn6NlCFB4iN1F+Q+lxQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     classcat "^5.0.3" | ||||||
|  |     zustand "^4.4.1" | ||||||
|  |  | ||||||
| "@rollup/plugin-babel@^5.2.0": | "@rollup/plugin-babel@^5.2.0": | ||||||
|   version "5.3.0" |   version "5.3.0" | ||||||
|   resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" |   resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" | ||||||
| @@ -3823,6 +3901,216 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/node" "*" |     "@types/node" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-array@*": | ||||||
|  |   version "3.2.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" | ||||||
|  |   integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== | ||||||
|  |  | ||||||
|  | "@types/d3-axis@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" | ||||||
|  |   integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-brush@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" | ||||||
|  |   integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-chord@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" | ||||||
|  |   integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== | ||||||
|  |  | ||||||
|  | "@types/d3-color@*": | ||||||
|  |   version "3.1.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" | ||||||
|  |   integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== | ||||||
|  |  | ||||||
|  | "@types/d3-contour@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" | ||||||
|  |   integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-array" "*" | ||||||
|  |     "@types/geojson" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-delaunay@*": | ||||||
|  |   version "6.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" | ||||||
|  |   integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== | ||||||
|  |  | ||||||
|  | "@types/d3-dispatch@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" | ||||||
|  |   integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== | ||||||
|  |  | ||||||
|  | "@types/d3-drag@*", "@types/d3-drag@^3.0.1": | ||||||
|  |   version "3.0.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" | ||||||
|  |   integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-dsv@*": | ||||||
|  |   version "3.0.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" | ||||||
|  |   integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== | ||||||
|  |  | ||||||
|  | "@types/d3-ease@*": | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" | ||||||
|  |   integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== | ||||||
|  |  | ||||||
|  | "@types/d3-fetch@*": | ||||||
|  |   version "3.0.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" | ||||||
|  |   integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-dsv" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-force@*": | ||||||
|  |   version "3.0.9" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.9.tgz#dd96ccefba4386fe4ff36b8e4ee4e120c21fcf29" | ||||||
|  |   integrity sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA== | ||||||
|  |  | ||||||
|  | "@types/d3-format@*": | ||||||
|  |   version "3.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" | ||||||
|  |   integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== | ||||||
|  |  | ||||||
|  | "@types/d3-geo@*": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" | ||||||
|  |   integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/geojson" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-hierarchy@*": | ||||||
|  |   version "3.1.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" | ||||||
|  |   integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== | ||||||
|  |  | ||||||
|  | "@types/d3-interpolate@*": | ||||||
|  |   version "3.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" | ||||||
|  |   integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-color" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-path@*": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" | ||||||
|  |   integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== | ||||||
|  |  | ||||||
|  | "@types/d3-polygon@*": | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" | ||||||
|  |   integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== | ||||||
|  |  | ||||||
|  | "@types/d3-quadtree@*": | ||||||
|  |   version "3.0.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" | ||||||
|  |   integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== | ||||||
|  |  | ||||||
|  | "@types/d3-random@*": | ||||||
|  |   version "3.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" | ||||||
|  |   integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== | ||||||
|  |  | ||||||
|  | "@types/d3-scale-chromatic@*": | ||||||
|  |   version "3.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" | ||||||
|  |   integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== | ||||||
|  |  | ||||||
|  | "@types/d3-scale@*": | ||||||
|  |   version "4.0.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" | ||||||
|  |   integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-time" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-selection@*", "@types/d3-selection@^3.0.3": | ||||||
|  |   version "3.0.10" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe" | ||||||
|  |   integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg== | ||||||
|  |  | ||||||
|  | "@types/d3-shape@*": | ||||||
|  |   version "3.1.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" | ||||||
|  |   integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-path" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-time-format@*": | ||||||
|  |   version "4.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" | ||||||
|  |   integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== | ||||||
|  |  | ||||||
|  | "@types/d3-time@*": | ||||||
|  |   version "3.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" | ||||||
|  |   integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== | ||||||
|  |  | ||||||
|  | "@types/d3-timer@*": | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" | ||||||
|  |   integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== | ||||||
|  |  | ||||||
|  | "@types/d3-transition@*": | ||||||
|  |   version "3.0.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f" | ||||||
|  |   integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": | ||||||
|  |   version "3.0.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" | ||||||
|  |   integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-interpolate" "*" | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |  | ||||||
|  | "@types/d3@^7.4.0": | ||||||
|  |   version "7.4.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" | ||||||
|  |   integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-array" "*" | ||||||
|  |     "@types/d3-axis" "*" | ||||||
|  |     "@types/d3-brush" "*" | ||||||
|  |     "@types/d3-chord" "*" | ||||||
|  |     "@types/d3-color" "*" | ||||||
|  |     "@types/d3-contour" "*" | ||||||
|  |     "@types/d3-delaunay" "*" | ||||||
|  |     "@types/d3-dispatch" "*" | ||||||
|  |     "@types/d3-drag" "*" | ||||||
|  |     "@types/d3-dsv" "*" | ||||||
|  |     "@types/d3-ease" "*" | ||||||
|  |     "@types/d3-fetch" "*" | ||||||
|  |     "@types/d3-force" "*" | ||||||
|  |     "@types/d3-format" "*" | ||||||
|  |     "@types/d3-geo" "*" | ||||||
|  |     "@types/d3-hierarchy" "*" | ||||||
|  |     "@types/d3-interpolate" "*" | ||||||
|  |     "@types/d3-path" "*" | ||||||
|  |     "@types/d3-polygon" "*" | ||||||
|  |     "@types/d3-quadtree" "*" | ||||||
|  |     "@types/d3-random" "*" | ||||||
|  |     "@types/d3-scale" "*" | ||||||
|  |     "@types/d3-scale-chromatic" "*" | ||||||
|  |     "@types/d3-selection" "*" | ||||||
|  |     "@types/d3-shape" "*" | ||||||
|  |     "@types/d3-time" "*" | ||||||
|  |     "@types/d3-time-format" "*" | ||||||
|  |     "@types/d3-timer" "*" | ||||||
|  |     "@types/d3-transition" "*" | ||||||
|  |     "@types/d3-zoom" "*" | ||||||
|  |  | ||||||
| "@types/debug@^4.1.7": | "@types/debug@^4.1.7": | ||||||
|   version "4.1.8" |   version "4.1.8" | ||||||
|   resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz" |   resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz" | ||||||
| @@ -3913,6 +4201,11 @@ | |||||||
|     "@types/qs" "*" |     "@types/qs" "*" | ||||||
|     "@types/serve-static" "*" |     "@types/serve-static" "*" | ||||||
|  |  | ||||||
|  | "@types/geojson@*": | ||||||
|  |   version "7946.0.14" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" | ||||||
|  |   integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== | ||||||
|  |  | ||||||
| "@types/graceful-fs@^4.1.2": | "@types/graceful-fs@^4.1.2": | ||||||
|   version "4.1.5" |   version "4.1.5" | ||||||
|   resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" |   resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" | ||||||
| @@ -6044,6 +6337,11 @@ cjs-module-lexer@^1.0.0: | |||||||
|   resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" |   resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" | ||||||
|   integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== |   integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== | ||||||
|  |  | ||||||
|  | classcat@^5.0.3, classcat@^5.0.4: | ||||||
|  |   version "5.0.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" | ||||||
|  |   integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== | ||||||
|  |  | ||||||
| clean-css@^5.2.2: | clean-css@^5.2.2: | ||||||
|   version "5.2.2" |   version "5.2.2" | ||||||
|   resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz" |   resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz" | ||||||
| @@ -6829,6 +7127,68 @@ csstype@^3.1.1: | |||||||
|   resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" |   resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" | ||||||
|   integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== |   integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== | ||||||
|  |  | ||||||
|  | "d3-color@1 - 3": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" | ||||||
|  |   integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== | ||||||
|  |  | ||||||
|  | "d3-dispatch@1 - 3": | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" | ||||||
|  |   integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== | ||||||
|  |  | ||||||
|  | "d3-drag@2 - 3", d3-drag@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" | ||||||
|  |   integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== | ||||||
|  |   dependencies: | ||||||
|  |     d3-dispatch "1 - 3" | ||||||
|  |     d3-selection "3" | ||||||
|  |  | ||||||
|  | "d3-ease@1 - 3": | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" | ||||||
|  |   integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== | ||||||
|  |  | ||||||
|  | "d3-interpolate@1 - 3": | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" | ||||||
|  |   integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== | ||||||
|  |   dependencies: | ||||||
|  |     d3-color "1 - 3" | ||||||
|  |  | ||||||
|  | "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" | ||||||
|  |   integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== | ||||||
|  |  | ||||||
|  | "d3-timer@1 - 3": | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" | ||||||
|  |   integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== | ||||||
|  |  | ||||||
|  | "d3-transition@2 - 3": | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" | ||||||
|  |   integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== | ||||||
|  |   dependencies: | ||||||
|  |     d3-color "1 - 3" | ||||||
|  |     d3-dispatch "1 - 3" | ||||||
|  |     d3-ease "1 - 3" | ||||||
|  |     d3-interpolate "1 - 3" | ||||||
|  |     d3-timer "1 - 3" | ||||||
|  |  | ||||||
|  | d3-zoom@^3.0.0: | ||||||
|  |   version "3.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" | ||||||
|  |   integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== | ||||||
|  |   dependencies: | ||||||
|  |     d3-dispatch "1 - 3" | ||||||
|  |     d3-drag "2 - 3" | ||||||
|  |     d3-interpolate "1 - 3" | ||||||
|  |     d3-selection "2 - 3" | ||||||
|  |     d3-transition "2 - 3" | ||||||
|  |  | ||||||
| damerau-levenshtein@^1.0.7: | damerau-levenshtein@^1.0.7: | ||||||
|   version "1.0.8" |   version "1.0.8" | ||||||
|   resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" |   resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" | ||||||
| @@ -13809,6 +14169,18 @@ react@^18.2.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     loose-envify "^1.1.0" |     loose-envify "^1.1.0" | ||||||
|  |  | ||||||
|  | reactflow@^11.11.2: | ||||||
|  |   version "11.11.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.2.tgz#4968866a9372e6004ad1e424a2141996f0ba769a" | ||||||
|  |   integrity sha512-o1fT3stSdhzW+SedCGNSmEvZvULZygZIMLyW67NcWNZrgwx1wuJfzLg5fuQ0Nzf389wItumZX/zP3zdaPX7lEw== | ||||||
|  |   dependencies: | ||||||
|  |     "@reactflow/background" "11.3.12" | ||||||
|  |     "@reactflow/controls" "11.2.12" | ||||||
|  |     "@reactflow/core" "11.11.2" | ||||||
|  |     "@reactflow/minimap" "11.7.12" | ||||||
|  |     "@reactflow/node-resizer" "2.2.12" | ||||||
|  |     "@reactflow/node-toolbar" "1.3.12" | ||||||
|  |  | ||||||
| read-cmd-shim@^2.0.0: | read-cmd-shim@^2.0.0: | ||||||
|   version "2.0.0" |   version "2.0.0" | ||||||
|   resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz" |   resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz" | ||||||
| @@ -15977,6 +16349,11 @@ url-parse-lax@^3.0.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     prepend-http "^2.0.0" |     prepend-http "^2.0.0" | ||||||
|  |  | ||||||
|  | use-sync-external-store@1.2.0: | ||||||
|  |   version "1.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" | ||||||
|  |   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== | ||||||
|  |  | ||||||
| util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: | util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: | ||||||
|   version "1.0.2" |   version "1.0.2" | ||||||
|   resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" |   resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" | ||||||
| @@ -16948,3 +17325,10 @@ zen-observable@0.8.15: | |||||||
|   version "0.8.15" |   version "0.8.15" | ||||||
|   resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz" |   resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz" | ||||||
|   integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== |   integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== | ||||||
|  |  | ||||||
|  | zustand@^4.4.1: | ||||||
|  |   version "4.5.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848" | ||||||
|  |   integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g== | ||||||
|  |   dependencies: | ||||||
|  |     use-sync-external-store "1.2.0" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user