Compare commits

..

4 Commits

Author SHA1 Message Date
Rıdvan Akca
7c1d2c47ec feat(bluesky): add reply to post action 2024-05-28 10:05:47 +02:00
Rıdvan Akca
84ecf105c2 feat(bluesky): add search post by url action 2024-05-28 10:05:09 +02:00
Rıdvan Akca
c07a02ef31 feat(bluesky): add create post action 2024-05-28 10:04:19 +02:00
Rıdvan Akca
3c44f55f19 feat(bluesky): add bluesky integration 2024-05-28 10:03:49 +02:00
67 changed files with 646 additions and 1475 deletions

View File

@@ -20,6 +20,7 @@
"db:migrate": "node ./bin/database/convert-migrations.js && knex migrate:latest"
},
"dependencies": {
"@atproto/api": "^0.12.13",
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",

View File

@@ -0,0 +1,41 @@
import { BskyAgent, RichText } from '@atproto/api';
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Create post',
key: 'createPost',
description: 'Creates a new post.',
arguments: [
{
label: 'Text',
key: 'text',
type: 'string',
required: true,
variables: true,
description: '',
},
],
async run($) {
const text = $.step.parameters.text;
const agent = new BskyAgent({ service: 'https://bsky.social/xrpc' });
const richText = new RichText({ text });
await richText.detectFacets(agent);
const body = {
repo: $.auth.data.did,
collection: 'app.bsky.feed.post',
record: {
$type: 'app.bsky.feed.post',
text: richText.text,
facets: richText.facets,
createdAt: new Date().toISOString(),
},
};
const { data } = await $.http.post('/com.atproto.repo.createRecord', body);
$.setActionItem({ raw: data });
},
});

View File

@@ -0,0 +1,5 @@
import createPost from './create-post/index.js';
import replyToPost from './reply-to-post/index.js';
import searchPostByUrl from './search-post-by-url/index.js';
export default [createPost, replyToPost, searchPostByUrl];

View File

@@ -0,0 +1,55 @@
import { BskyAgent, RichText } from '@atproto/api';
import defineAction from '../../../../helpers/define-action.js';
import getReplyRefs from '../../common/get-reply-refs.js';
export default defineAction({
name: 'Reply to post',
key: 'replyToPost',
description: 'Replies to a post.',
arguments: [
{
label: 'Post Url',
key: 'postUrl',
type: 'string',
required: true,
variables: true,
description: 'Enter whole post url you want to reply here.',
},
{
label: 'Text',
key: 'text',
type: 'string',
required: true,
variables: true,
description: 'Your post.',
},
],
async run($) {
const text = $.step.parameters.text;
const postUrl = $.step.parameters.postUrl;
const replyRefs = await getReplyRefs($, postUrl);
const agent = new BskyAgent({ service: 'https://bsky.social/xrpc' });
const richText = new RichText({ text });
await richText.detectFacets(agent);
const body = {
repo: $.auth.data.did,
collection: 'app.bsky.feed.post',
record: {
$type: 'app.bsky.feed.post',
text: richText.text,
facets: richText.facets,
createdAt: new Date().toISOString(),
reply: replyRefs,
},
};
const { data } = await $.http.post('/com.atproto.repo.createRecord', body);
$.setActionItem({ raw: data });
},
});

View File

@@ -0,0 +1,35 @@
import defineAction from '../../../../helpers/define-action.js';
export default defineAction({
name: 'Search post by url',
key: 'searchPostByUrl',
description: 'Searches a post in a thread by url.',
arguments: [
{
label: 'Post Url',
key: 'postUrl',
type: 'string',
required: true,
variables: true,
description: 'Enter whole post url here.',
},
],
async run($) {
const postUrl = $.step.parameters.postUrl;
const urlParts = postUrl.split('/');
const handle = urlParts[urlParts.length - 3];
const postId = urlParts[urlParts.length - 1];
const uri = `at://${handle}/app.bsky.feed.post/${postId}`;
const params = {
uri,
};
const { data } = await $.http.get('/app.bsky.feed.getPostThread', {
params,
});
$.setActionItem({ raw: data });
},
});

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@@ -0,0 +1,34 @@
import verifyCredentials from './verify-credentials.js';
import isStillVerified from './is-still-verified.js';
import refreshToken from './refresh-token.js';
export default {
fields: [
{
key: 'handle',
label: 'Your Bluesky Handle',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: '',
clickToCopy: false,
},
{
key: 'password',
label: 'Your Bluesky Password',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: '',
clickToCopy: false,
},
],
verifyCredentials,
isStillVerified,
refreshToken,
};

View File

@@ -0,0 +1,8 @@
import getCurrentUser from '../common/get-current-user.js';
const isStillVerified = async ($) => {
const currentUser = await getCurrentUser($);
return !!currentUser.did;
};
export default isStillVerified;

View File

@@ -0,0 +1,24 @@
const refreshToken = async ($) => {
const { refreshJwt } = $.auth.data;
const { data } = await $.http.post(
'/com.atproto.server.refreshSession',
null,
{
headers: {
Authorization: `Bearer ${refreshJwt}`,
},
additionalProperties: {
skipAddingAuthHeader: true,
},
}
);
await $.auth.set({
accessJwt: data.accessJwt,
refreshJwt: data.refreshJwt,
did: data.did,
});
};
export default refreshToken;

View File

@@ -0,0 +1,20 @@
const verifyCredentials = async ($) => {
const handle = $.auth.data.handle;
const password = $.auth.data.password;
const body = {
identifier: handle,
password,
};
const { data } = await $.http.post('/com.atproto.server.createSession', body);
await $.auth.set({
accessJwt: data.accessJwt,
refreshJwt: data.refreshJwt,
did: data.did,
screenName: data.handle,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,12 @@
const addAuthHeader = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthHeader)
return requestConfig;
if ($.auth.data?.accessJwt) {
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessJwt}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,15 @@
const getCurrentUser = async ($) => {
const handle = $.auth.data.handle;
const params = {
actor: handle,
};
const { data: currentUser } = await $.http.get('/app.bsky.actor.getProfile', {
params,
});
return currentUser;
};
export default getCurrentUser;

View File

@@ -0,0 +1,58 @@
const parseUri = (uri) => {
const parts = uri.split('/');
return {
repo: parts[4],
collection: 'app.bsky.feed.post',
rkey: parts[6],
};
};
const getReplyRefs = async ($, parentUri) => {
const uriParts = parseUri(parentUri);
try {
const { data: parent } = await $.http.get('/com.atproto.repo.getRecord', {
params: uriParts,
});
const parentReply = parent.value?.reply;
let root;
if (parentReply) {
const rootUri = parentReply.root.uri;
const [rootRepo, rootCollection, rootRkey] = rootUri
.split('/')
.slice(2, 5);
const params = {
repo: rootRepo,
collection: rootCollection,
rkey: rootRkey,
};
const rootResp = await $.http.get('/com.atproto.repo.getRecord', {
params,
});
root = rootResp.data;
} else {
root = parent;
}
return {
root: {
uri: root.uri,
cid: root.cid,
},
parent: {
uri: parent.uri,
cid: parent.cid,
},
};
} catch (error) {
throw new Error('Error while fetching records');
}
};
export default getReplyRefs;

View File

@@ -0,0 +1,18 @@
import defineApp from '../../helpers/define-app.js';
import addAuthHeader from './common/add-auth-header.js';
import auth from './auth/index.js';
import actions from './actions/index.js';
export default defineApp({
name: 'Bluesky',
key: 'bluesky',
iconUrl: '{BASE_URL}/apps/bluesky/assets/favicon.svg',
authDocUrl: '{DOCS_URL}/apps/bluesky/connection',
supportsConnections: true,
baseUrl: 'https://bluesky.app',
apiBaseUrl: 'https://bsky.social/xrpc',
primaryColor: '1185fd',
beforeRequest: [addAuthHeader],
auth,
actions,
});

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development',
isTest: appEnv === 'test',
isProd: appEnv === 'production',
version: '0.12.0',
version: '0.11.0',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.12.0',
version: '0.11.0',
},
meta: {
count: 1,

View File

@@ -1,11 +0,0 @@
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();
});
}

View File

@@ -33,8 +33,8 @@ class User extends Base {
fullName: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 },
password: { type: 'string' },
resetPasswordToken: { type: ['string', 'null'] },
resetPasswordTokenSentAt: { type: ['string', 'null'], format: 'date-time' },
resetPasswordToken: { type: 'string' },
resetPasswordTokenSentAt: { type: 'string' },
trialExpiryDate: { type: 'string' },
roleId: { type: 'string', format: 'uuid' },
deletedAt: { type: 'string' },

View File

@@ -40,7 +40,6 @@ export const worker = new Worker(
await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
}
await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
await user.$query().withSoftDeleted().hardDelete();
},
{ connection: redisConfig }

View File

@@ -50,6 +50,15 @@ export default defineConfig({
{ text: 'Connection', link: '/apps/appwrite/connection' },
],
},
{
text: 'Bluesky',
collapsible: true,
collapsed: true,
items: [
{ text: 'Actions', link: '/apps/bluesky/actions' },
{ text: 'Connection', link: '/apps/bluesky/connection' },
],
},
{
text: 'Carbone',
collapsible: true,

View File

@@ -0,0 +1,16 @@
---
favicon: /favicons/bluesky.svg
items:
- name: Create post
desc: Creates a new post.
- name: Reply to post
desc: Replies to a post.
- name: Search post by url
desc: Searches a post in a thread by url.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -0,0 +1,10 @@
# Bluesky
:::info
This page explains the steps you need to follow to set up the Bluesky connection in Automatisch. If any of the steps are outdated, please let us know!
:::
1. Enter your `Bluesky Handle` from the page to the `Your Bluesky Handle` field on Automatisch.
1. Enter your `Bluesky Password` from the page to the `Your Bluesky Password` field on Automatisch.
1. Click **Submit** button on Automatisch.
1. Congrats! Start using your new Bluesky connection within the flows.

View File

@@ -6,12 +6,16 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
.
├── packages
│   ├── backend
│   ├── cli
│   ├── docs
│   ├── e2e-tests
│   ├── types
│   └── web
```
- `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.
- `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.

View File

@@ -4,6 +4,7 @@ The following integrations are currently supported by Automatisch.
- [Airtable](/apps/airtable/actions)
- [Appwrite](/apps/appwrite/triggers)
- [Bluesky](/apps/bluesky/actions)
- [Carbone](/apps/carbone/actions)
- [Datastore](/apps/datastore/actions)
- [DeepL](/apps/deepl/actions)

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-50 -50 430 390" fill="#1185fd" aria-hidden="true">
<path d="M180 141.964C163.699 110.262 119.308 51.1817 78.0347 22.044C38.4971 -5.86834 23.414 -1.03207 13.526 3.43594C2.08093 8.60755 0 26.1785 0 36.5164C0 46.8542 5.66748 121.272 9.36416 133.694C21.5786 174.738 65.0603 188.607 105.104 184.156C107.151 183.852 109.227 183.572 111.329 183.312C109.267 183.642 107.19 183.924 105.104 184.156C46.4204 192.847 -5.69621 214.233 62.6582 290.33C137.848 368.18 165.705 273.637 180 225.702C194.295 273.637 210.76 364.771 295.995 290.33C360 225.702 313.58 192.85 254.896 184.158C252.81 183.926 250.733 183.645 248.671 183.315C250.773 183.574 252.849 183.855 254.896 184.158C294.94 188.61 338.421 174.74 350.636 133.697C354.333 121.275 360 46.8568 360 36.519C360 26.1811 357.919 8.61012 346.474 3.43851C336.586 -1.02949 321.503 -5.86576 281.965 22.0466C240.692 51.1843 196.301 110.262 180 141.964Z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@@ -7,7 +7,6 @@
"@apollo/client": "^3.6.9",
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
"@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@hookform/resolvers": "^2.8.8",
@@ -33,7 +32,6 @@
"react-router-dom": "^6.0.2",
"react-scripts": "5.0.0",
"react-window": "^1.8.9",
"reactflow": "^11.11.2",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.94.2",

View File

@@ -68,10 +68,7 @@ function AccountDropdownMenu(props) {
AccountDropdownMenu.propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
id: PropTypes.string.isRequired,
};

View File

@@ -36,7 +36,7 @@ function AdminApplicationSettings(props) {
const handleSubmit = async (values) => {
try {
if (!appConfig?.data) {
if (!appConfig.data) {
await createAppConfig({
variables: {
input: { key: props.appKey, ...values },
@@ -69,7 +69,6 @@ function AdminApplicationSettings(props) {
}),
[appConfig?.data],
);
return (
<Form
defaultValues={defaultValues}

View File

@@ -8,7 +8,6 @@ import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { ConnectionPropType } from 'propTypes/propTypes';
import { useQueryClient } from '@tanstack/react-query';
import Can from 'components/Can';
function ContextMenu(props) {
const {
@@ -45,57 +44,34 @@ function ContextMenu(props) {
hideBackdrop={false}
anchorEl={anchorEl}
>
<Can I="read" a="Flow" passThrough>
{(allowed) => (
<MenuItem
component={Link}
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'viewFlows' })}
disabled={!allowed}
>
{formatMessage('connection.viewFlows')}
</MenuItem>
)}
</Can>
<MenuItem
component={Link}
to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'viewFlows' })}
>
{formatMessage('connection.viewFlows')}
</MenuItem>
<Can I="update" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'test' })}
disabled={!allowed}
>
{formatMessage('connection.testConnection')}
</MenuItem>
)}
</Can>
<MenuItem onClick={createActionHandler({ type: 'test' })}>
{formatMessage('connection.testConnection')}
</MenuItem>
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<MenuItem
component={Link}
disabled={!allowed || disableReconnection}
to={URLS.APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId,
)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('connection.reconnect')}
</MenuItem>
<MenuItem
component={Link}
disabled={disableReconnection}
to={URLS.APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId,
)}
</Can>
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('connection.reconnect')}
</MenuItem>
<Can I="delete" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'delete' })}
disabled={!allowed}
>
{formatMessage('connection.delete')}
</MenuItem>
)}
</Can>
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
{formatMessage('connection.delete')}
</MenuItem>
</Menu>
);
}

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import AppConnectionRow from 'components/AppConnectionRow';
import NoResultFound from 'components/NoResultFound';
import Can from 'components/Can';
import useFormatMessage from 'hooks/useFormatMessage';
import * as URLS from 'config/urls';
import useAppConnections from 'hooks/useAppConnections';
@@ -17,15 +16,11 @@ function AppConnections(props) {
if (!hasConnections) {
return (
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noConnections')}
data-test="connections-no-results"
{...(allowed && { to: URLS.APP_ADD_CONNECTION(appKey) })}
/>
)}
</Can>
<NoResultFound
to={URLS.APP_ADD_CONNECTION(appKey)}
text={formatMessage('app.noConnections')}
data-test="connections-no-results"
/>
);
}

View File

@@ -5,7 +5,6 @@ import PaginationItem from '@mui/material/PaginationItem';
import * as URLS from 'config/urls';
import AppFlowRow from 'components/FlowRow';
import Can from 'components/Can';
import NoResultFound from 'components/NoResultFound';
import useFormatMessage from 'hooks/useFormatMessage';
import useConnectionFlows from 'hooks/useConnectionFlows';
@@ -37,20 +36,11 @@ function AppFlows(props) {
if (!hasFlows) {
return (
<Can I="create" a="Flow" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noFlows')}
data-test="flows-no-results"
{...(allowed && {
to: URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId
),
})}
/>
)}
</Can>
<NoResultFound
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)}
text={formatMessage('app.noFlows')}
data-test="flows-no-results"
/>
);
}

View File

@@ -165,7 +165,6 @@ function ChooseAppAndEventSubstep(props) {
value={getOption(appOptions, step.appKey) || null}
onChange={onAppChange}
data-test="choose-app-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
{step.appKey && (
@@ -228,7 +227,6 @@ function ChooseAppAndEventSubstep(props) {
value={getOption(actionOrTriggerOptions, step.key) || null}
onChange={onEventChange}
data-test="choose-event-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
</Box>
)}

View File

@@ -240,7 +240,6 @@ function ChooseConnectionSubstep(props) {
onChange={handleChange}
loading={isAppConnectionsLoading}
data-test="choose-connection-autocomplete"
componentsProps={{ popper: { className: 'nowheel' } }}
/>
<Button

View File

@@ -32,11 +32,9 @@ function ControlledAutocomplete(props) {
...autocompleteProps
} = props;
let dependsOnValues = [];
if (dependsOn?.length) {
dependsOnValues = watch(dependsOn);
}
React.useEffect(() => {
const hasDependencies = dependsOnValues.length;
const allDepsSatisfied = dependsOnValues.every(Boolean);
@@ -46,7 +44,6 @@ function ControlledAutocomplete(props) {
resetField(name);
}
}, dependsOnValues);
return (
<Controller
rules={{ required }}

View File

@@ -21,9 +21,7 @@ const CustomOptions = (props) => {
label,
initialTabIndex,
} = props;
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
React.useEffect(
function applyInitialActiveTabIndex() {
setActiveTabIndex((currentActiveTabIndex) => {
@@ -35,7 +33,6 @@ const CustomOptions = (props) => {
},
[initialTabIndex],
);
return (
<Popper
open={open}
@@ -50,7 +47,6 @@ const CustomOptions = (props) => {
},
},
]}
className="nowheel"
>
<Paper elevation={5} sx={{ width: '100%' }}>
<Tabs
@@ -79,10 +75,7 @@ const CustomOptions = (props) => {
CustomOptions.propTypes = {
open: PropTypes.bool.isRequired,
anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
data: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,

View File

@@ -61,7 +61,6 @@ function ControlledCustomAutocomplete(props) {
const [isSingleChoice, setSingleChoice] = React.useState(undefined);
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef(null);
const mountedRef = React.useRef(false);
const renderElement = React.useCallback(
(props) => <Element {...props} disabled={disabled} />,
@@ -95,14 +94,10 @@ function ControlledCustomAutocomplete(props) {
}, []);
React.useEffect(() => {
if (mountedRef.current) {
const hasDependencies = dependsOnValues.length;
if (hasDependencies) {
// Reset the field when a dependent has been updated
resetEditor(editor);
}
} else {
mountedRef.current = true;
const hasDependencies = dependsOnValues.length;
if (hasDependencies) {
// Reset the field when a dependent has been updated
resetEditor(editor);
}
}, dependsOnValues);

View File

@@ -64,19 +64,11 @@ function DynamicField(props) {
<Stack
direction={{ xs: 'column', sm: 'row' }}
spacing={{ xs: 2 }}
sx={{
display: 'flex',
flex: 1,
minWidth: 0,
}}
sx={{ display: 'flex', flex: 1 }}
>
{fields.map((fieldSchema, fieldSchemaIndex) => (
<Box
sx={{
display: 'flex',
flex: '1 0 0px',
minWidth: 0,
}}
sx={{ display: 'flex', flex: '1 0 0px' }}
key={`field-${field.__id}-${fieldSchemaIndex}`}
>
<InputCreator

View File

@@ -8,7 +8,6 @@ import Tooltip from '@mui/material/Tooltip';
import IconButton from '@mui/material/IconButton';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import Snackbar from '@mui/material/Snackbar';
import { ReactFlowProvider } from 'reactflow';
import { EditorProvider } from 'contexts/Editor';
import EditableTypography from 'components/EditableTypography';
@@ -21,9 +20,6 @@ import * as URLS from 'config/urls';
import { TopBar } from './style';
import useFlow from 'hooks/useFlow';
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() {
const { flowId } = useParams();
@@ -59,25 +55,23 @@ export default function EditorLayout() {
const onFlowStatusUpdate = React.useCallback(
async (active) => {
try {
await updateFlowStatus({
variables: {
input: {
id: flowId,
active,
},
await updateFlowStatus({
variables: {
input: {
id: flowId,
active,
},
optimisticResponse: {
updateFlowStatus: {
__typename: 'Flow',
id: flowId,
active,
},
},
optimisticResponse: {
updateFlowStatus: {
__typename: 'Flow',
id: flowId,
active,
},
});
},
});
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
} catch (err) {}
await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
},
[flowId, queryClient],
);
@@ -137,28 +131,15 @@ export default function EditorLayout() {
</Button>
</Box>
</TopBar>
<Stack direction="column" height="100%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}
{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%">
<Container maxWidth="md">
<EditorProvider value={{ readOnly: !!flow?.active }}>
{!flow && !isFlowLoading && 'not found'}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
)}
{flow && <Editor flow={flow} />}
</EditorProvider>
</Container>
</Stack>
<Snackbar
data-test="flow-cannot-edit-info-snackbar"

View File

@@ -1,57 +0,0 @@
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,
};

View File

@@ -1,277 +0,0 @@
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;

View File

@@ -1,60 +0,0 @@
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;

View File

@@ -1,14 +0,0 @@
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,
}));

View File

@@ -1,19 +0,0 @@
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;

View File

@@ -1,10 +0,0 @@
export const INVISIBLE_NODE_ID = 'invisible-node';
export const NODE_TYPES = {
FLOW_STEP: 'flowStep',
INVISIBLE: 'invisible',
};
export const EDGE_TYPES = {
ADD_NODE_EDGE: 'addNodeEdge',
};

View File

@@ -1,13 +0,0 @@
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',
},
}));

View File

@@ -1,69 +0,0 @@
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]);
};

View File

@@ -1,13 +0,0 @@
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]);
};

View File

@@ -1,88 +0,0 @@
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;
};

View File

@@ -28,12 +28,9 @@ function ContextMenu(props) {
variables: { input: { id: flowId } },
});
if (appKey) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
}
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
variant: 'success',
SnackbarProps: {
@@ -59,12 +56,9 @@ function ContextMenu(props) {
},
});
if (appKey) {
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
}
await queryClient.invalidateQueries({
queryKey: ['apps', appKey, 'flows'],
});
enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
variant: 'success',
});
@@ -116,7 +110,7 @@ ContextMenu.propTypes = {
]).isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string,
appKey: PropTypes.string.isRequired,
};
export default ContextMenu;

View File

@@ -38,24 +38,20 @@ function FlowRow(props) {
const contextButtonRef = React.useRef(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props;
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = (event) => {
event.preventDefault();
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
setAnchorEl(contextButtonRef.current);
};
const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10));
const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10));
const isUpdated = updatedAt > createdAt;
const relativeCreatedAt = createdAt.toRelative();
const relativeUpdatedAt = updatedAt.toRelative();
return (
<>
<Card sx={{ mb: 1 }} data-test="flow-row">
@@ -131,7 +127,7 @@ FlowRow.propTypes = {
flow: FlowPropType.isRequired,
onDeleteFlow: PropTypes.func,
onDuplicateFlow: PropTypes.func,
appKey: PropTypes.string,
appKey: PropTypes.string.isRequired,
};
export default FlowRow;

View File

@@ -11,6 +11,9 @@ import IconButton from '@mui/material/IconButton';
import ErrorIcon from '@mui/icons-material/Error';
import CircularProgress from '@mui/material/CircularProgress';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { EditorContext } from 'contexts/Editor';
import { StepExecutionsProvider } from 'contexts/StepExecutions';
import TestSubstep from 'components/TestSubstep';
@@ -30,18 +33,77 @@ import {
Header,
Wrapper,
} from './style';
import isEmpty from 'helpers/isEmpty';
import { StepPropType } from 'propTypes/propTypes';
import useTriggers from 'hooks/useTriggers';
import useActions from 'hooks/useActions';
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
import useActionSubsteps from 'hooks/useActionSubsteps';
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
import { validationSchemaResolver } from './validation';
import { isEqual } from 'lodash';
const validIcon = <CheckCircleIcon color="success" />;
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) {
const { collapsed, onChange, onContinue, flowId } = props;
const editorContext = React.useContext(EditorContext);
@@ -52,10 +114,6 @@ function FlowStep(props) {
const isAction = step.type === 'action';
const formatMessage = useFormatMessage();
const [currentSubstep, setCurrentSubstep] = React.useState(0);
const [formResolverContext, setFormResolverContext] = React.useState({
substeps: [],
additionalFields: {},
});
const useAppsOptions = {};
if (isTrigger) {
@@ -110,12 +168,6 @@ function FlowStep(props) {
? triggerSubstepsData
: actionSubstepsData || [];
React.useEffect(() => {
if (!isEqual(substeps, formResolverContext.substeps)) {
setFormResolverContext({ substeps, additionalFields: {} });
}
}, [substeps]);
const handleChange = React.useCallback(({ step }) => {
onChange(step);
}, []);
@@ -128,6 +180,11 @@ function FlowStep(props) {
handleChange({ step: val });
};
const stepValidationSchema = React.useMemo(
() => generateValidationSchema(substeps),
[substeps],
);
if (!apps?.data) {
return (
<CircularProgress
@@ -156,15 +213,6 @@ function FlowStep(props) {
value !== substepIndex ? substepIndex : null,
);
const addAdditionalFieldsValidation = (additionalFields) => {
if (additionalFields) {
setFormResolverContext((prev) => ({
...prev,
additionalFields: { ...prev.additionalFields, ...additionalFields },
}));
}
};
const validationStatusIcon =
step.status === 'completed' ? validIcon : errorIcon;
@@ -218,8 +266,7 @@ function FlowStep(props) {
<Form
defaultValues={step}
onSubmit={handleSubmit}
resolver={validationSchemaResolver}
context={formResolverContext}
resolver={stepValidationSchema}
>
<ChooseAppAndEventSubstep
expanded={currentSubstep === 0}
@@ -283,9 +330,6 @@ function FlowStep(props) {
onSubmit={expandNextStep}
onChange={handleChange}
step={step}
addAdditionalFieldsValidation={
addAdditionalFieldsValidation
}
/>
)}
</React.Fragment>
@@ -316,6 +360,7 @@ function FlowStep(props) {
FlowStep.propTypes = {
collapsed: PropTypes.bool,
step: StepPropType.isRequired,
index: PropTypes.number,
onOpen: PropTypes.func,
onClose: PropTypes.func,
onChange: PropTypes.func.isRequired,

View File

@@ -1,120 +0,0 @@
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);
}

View File

@@ -43,10 +43,7 @@ function FlowStepContextMenu(props) {
FlowStepContextMenu.propTypes = {
stepId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
anchorEl: PropTypes.element.isRequired,
deletable: PropTypes.bool.isRequired,
};

View File

@@ -19,9 +19,7 @@ function FlowSubstep(props) {
onCollapse,
onSubmit,
step,
addAdditionalFieldsValidation,
} = props;
const { name, arguments: args } = substep;
const editorContext = React.useContext(EditorContext);
const formContext = useFormContext();
@@ -56,7 +54,6 @@ function FlowSubstep(props) {
stepId={step.id}
disabled={editorContext.readOnly}
showOptionValue={true}
addAdditionalFieldsValidation={addAdditionalFieldsValidation}
/>
))}
</Stack>

View File

@@ -1,8 +1,6 @@
import * as React from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
const noop = () => null;
export default function Form(props) {
const {
children,
@@ -11,31 +9,24 @@ export default function Form(props) {
resolver,
render,
mode = 'all',
context,
...formProps
} = props;
const methods = useForm({
defaultValues,
reValidateMode: 'onBlur',
resolver,
mode,
context,
});
const form = useWatch({ control: methods.control });
/**
* For fields having `dependsOn` fields, we need to re-validate the form.
*/
React.useEffect(() => {
methods.trigger();
}, [methods.trigger, form]);
React.useEffect(() => {
methods.reset(defaultValues);
}, [defaultValues]);
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>

View File

@@ -23,9 +23,7 @@ export default function InputCreator(props) {
disabled,
showOptionValue,
shouldUnregister,
addAdditionalFieldsValidation,
} = props;
const {
key: name,
label,
@@ -35,7 +33,6 @@ export default function InputCreator(props) {
description,
type,
} = schema;
const { data, loading } = useDynamicData(stepId, schema);
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
useDynamicFields(stepId, schema);
@@ -43,10 +40,6 @@ export default function InputCreator(props) {
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
React.useEffect(() => {
addAdditionalFieldsValidation?.({ [name]: additionalFields });
}, [additionalFields]);
if (type === 'dynamic') {
return (
<DynamicField
@@ -87,7 +80,6 @@ export default function InputCreator(props) {
disabled={disabled}
showOptionValue={showOptionValue}
shouldUnregister={shouldUnregister}
componentsProps={{ popper: { className: 'nowheel' } }}
/>
)}

View File

@@ -5,10 +5,8 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
import CardActionArea from '@mui/material/CardActionArea';
import Typography from '@mui/material/Typography';
import { CardContent } from './style';
export default function NoResultFound(props) {
const { text, to } = props;
const ActionAreaLink = React.useMemo(
() =>
React.forwardRef(function InlineLink(linkProps, ref) {
@@ -17,12 +15,12 @@ export default function NoResultFound(props) {
}),
[to],
);
return (
<Card elevation={0}>
<CardActionArea component={ActionAreaLink} {...props}>
<CardContent>
{!!to && <AddCircleIcon color="primary" />}
<Typography variant="body1">{text}</Typography>
</CardContent>
</CardActionArea>

View File

@@ -17,7 +17,6 @@ import { StepExecutionsContext } from 'contexts/StepExecutions';
import Popper from './Popper';
import { processStepWithExecutions } from './data';
import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style';
const PowerInput = (props) => {
const { control } = useFormContext();
const {
@@ -32,41 +31,33 @@ const PowerInput = (props) => {
} = props;
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef(null);
const renderElement = React.useCallback(
(props) => <Element {...props} />,
[],
);
const [editor] = React.useState(() => customizeEditor(createEditor()));
const [showVariableSuggestions, setShowVariableSuggestions] =
React.useState(false);
const disappearSuggestionsOnShift = (event) => {
if (event.code === 'Tab') {
setShowVariableSuggestions(false);
}
};
const stepsWithVariables = React.useMemo(() => {
return processStepWithExecutions(priorStepsWithExecutions);
}, [priorStepsWithExecutions]);
const handleBlur = React.useCallback(
(value) => {
onBlur?.(value);
},
[onBlur],
);
const handleVariableSuggestionClick = React.useCallback(
(variable) => {
insertVariable(editor, variable, stepsWithVariables);
},
[stepsWithVariables],
);
return (
<Controller
rules={{ required }}
@@ -136,7 +127,6 @@ const PowerInput = (props) => {
anchorEl={editorRef.current}
data={stepsWithVariables}
onSuggestionClick={handleVariableSuggestionClick}
className="nowheel"
/>
<FormHelperText variant="outlined">{description}</FormHelperText>

View File

@@ -7,7 +7,6 @@ import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api';
const variableRegExp = /({.*?})/;
// TODO: extract this function to a separate file
function computeArguments(args, getValues) {
const initialValue = {};

View File

@@ -1,9 +0,0 @@
import { useEffect, useRef } from "react";
export const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};

View File

@@ -1,6 +1,4 @@
import { createRoot } from 'react-dom/client';
import { Settings } from 'luxon';
import ThemeProvider from 'components/ThemeProvider';
import IntlProvider from 'components/IntlProvider';
import ApolloProvider from 'components/ApolloProvider';
@@ -12,9 +10,6 @@ import Router from 'components/Router';
import routes from 'routes';
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 root = createRoot(container);

View File

@@ -30,7 +30,6 @@ import AppIcon from 'components/AppIcon';
import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import useApp from 'hooks/useApp';
import Can from 'components/Can';
const ReconnectConnection = (props) => {
const { application, onClose } = props;
@@ -93,7 +92,7 @@ export default function Application() {
}
return options;
}, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
}, [appKey, appConfig?.data, currentUserAbility]);
if (loading) return null;
@@ -119,46 +118,37 @@ export default function Application() {
<Route
path={`${URLS.FLOWS}/*`}
element={
<Can I="create" a="Flow" passThrough>
{(allowed) => (
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={Link}
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId,
)}
fullWidth
icon={<AddIcon />}
disabled={!allowed}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
<ConditionalIconButton
type="submit"
variant="contained"
color="primary"
size="large"
component={Link}
to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
appKey,
connectionId,
)}
</Can>
fullWidth
icon={<AddIcon />}
disabled={!currentUserAbility.can('create', 'Flow')}
>
{formatMessage('app.createFlow')}
</ConditionalIconButton>
}
/>
<Route
path={`${URLS.CONNECTIONS}/*`}
element={
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<SplitButton
disabled={
!allowed ||
(appConfig?.data &&
!appConfig?.data?.canConnect &&
!appConfig?.data?.canCustomConnect) ||
connectionOptions.every(({ disabled }) => disabled)
}
options={connectionOptions}
/>
)}
</Can>
<SplitButton
disabled={
(appConfig?.data &&
!appConfig?.data?.canConnect &&
!appConfig?.data?.canCustomConnect) ||
connectionOptions.every(({ disabled }) => disabled)
}
options={connectionOptions}
/>
}
/>
</Routes>
@@ -179,20 +169,17 @@ export default function Application() {
label={formatMessage('app.connections')}
to={URLS.APP_CONNECTIONS(appKey)}
value={URLS.APP_CONNECTIONS_PATTERN}
disabled={
!currentUserAbility.can('read', 'Connection') ||
!app.supportsConnections
}
disabled={!app.supportsConnections}
component={Link}
data-test="connections-tab"
/>
<Tab
label={formatMessage('app.flows')}
to={URLS.APP_FLOWS(appKey)}
value={URLS.APP_FLOWS_PATTERN}
component={Link}
data-test="flows-tab"
disabled={!currentUserAbility.can('read', 'Flow')}
/>
</Tabs>
</Box>
@@ -200,20 +187,14 @@ export default function Application() {
<Routes>
<Route
path={`${URLS.FLOWS}/*`}
element={
<Can I="read" a="Flow">
<AppFlows appKey={appKey} />
</Can>
}
element={<AppFlows appKey={appKey} />}
/>
<Route
path={`${URLS.CONNECTIONS}/*`}
element={
<Can I="read" a="Connection">
<AppConnections appKey={appKey} />
</Can>
}
element={<AppConnections appKey={appKey} />}
/>
<Route
path="/"
element={
@@ -237,24 +218,17 @@ export default function Application() {
<Route
path="/connections/add"
element={
<Can I="create" a="Connection">
<AddAppConnection
onClose={goToApplicationPage}
application={app}
/>
</Can>
<AddAppConnection onClose={goToApplicationPage} application={app} />
}
/>
<Route
path="/connections/:connectionId/reconnect"
element={
<Can I="create" a="Connection">
<ReconnectConnection
application={app}
onClose={goToApplicationPage}
/>
</Can>
<ReconnectConnection
application={app}
onClose={goToApplicationPage}
/>
}
/>
</Routes>

View File

@@ -84,14 +84,10 @@ export default function Applications() {
)}
{!isLoading && !hasApps && (
<Can I="create" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('apps.noConnections')}
{...(allowed && { to: URLS.NEW_APP_CONNECTION })}
/>
)}
</Can>
<NoResultFound
text={formatMessage('apps.noConnections')}
to={URLS.NEW_APP_CONNECTION}
/>
)}
{!isLoading &&

View File

@@ -7,15 +7,13 @@ import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import { CREATE_FLOW } from 'graphql/mutations/create-flow';
import Box from '@mui/material/Box';
export default function CreateFlow() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const formatMessage = useFormatMessage();
const [createFlow, { error }] = useMutation(CREATE_FLOW);
const [createFlow] = useMutation(CREATE_FLOW);
const appKey = searchParams.get('appKey');
const connectionId = searchParams.get('connectionId');
React.useEffect(() => {
async function initiate() {
const variables = {};
@@ -35,11 +33,6 @@ export default function CreateFlow() {
}
initiate();
}, [createFlow, navigate, appKey, connectionId]);
if (error) {
return null;
}
return (
<Box
sx={{
@@ -52,6 +45,7 @@ export default function CreateFlow() {
}}
>
<CircularProgress size={16} thickness={7.5} />
<Typography variant="body2">
{formatMessage('createFlow.creating')}
</Typography>

View File

@@ -17,7 +17,6 @@ import Container from 'components/Container';
import PageTitle from 'components/PageTitle';
import SearchInput from 'components/SearchInput';
import useFormatMessage from 'hooks/useFormatMessage';
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
import * as URLS from 'config/urls';
import useLazyFlows from 'hooks/useLazyFlows';
@@ -27,7 +26,6 @@ export default function Flows() {
const page = parseInt(searchParams.get('page') || '', 10) || 1;
const [flowName, setFlowName] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const currentUserAbility = useCurrentUserAbility();
const { data, mutate: fetchFlows } = useLazyFlows(
{ flowName, page },
@@ -126,9 +124,7 @@ export default function Flows() {
{!isLoading && !hasFlows && (
<NoResultFound
text={formatMessage('flows.noFlows')}
{...(currentUserAbility.can('create', 'Flow') && {
to: URLS.CREATE_FLOW,
})}
to={URLS.CREATE_FLOW}
/>
)}
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (

View File

@@ -3,7 +3,7 @@ services:
name: automatisch-main
env: docker
dockerfilePath: ./docker/Dockerfile
dockerContext: .
dockerContext: ./docker
repo: https://github.com/automatisch/automatisch
autoDeploy: false
envVars:
@@ -47,7 +47,7 @@ services:
name: automatisch-worker
env: docker
dockerfilePath: ./docker/Dockerfile
dockerContext: .
dockerContext: ./docker
repo: https://github.com/automatisch/automatisch
autoDeploy: false
envVars:

460
yarn.lock
View File

@@ -170,6 +170,52 @@
tslib "^2.3.0"
zen-observable-ts "~1.1.0"
"@atproto/api@^0.12.13":
version "0.12.13"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.13.tgz#269d6c57ea894e23f20b28bd3cbfed944bd28528"
integrity sha512-pRSID6w8AUiZJoCxgctMPRTSGVFHq7wphAnxEbRLBP3OQ1g+BRZUcqFw+e+17Pd3wrc8VImjiD4HCWtCJvCx3w==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
"@atproto/syntax" "^0.3.0"
"@atproto/xrpc" "^0.5.0"
multiformats "^9.9.0"
tlds "^1.234.0"
"@atproto/common-web@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.3.0.tgz#36da8c2c31d8cf8a140c3c8f03223319bf4430bb"
integrity sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==
dependencies:
graphemer "^1.4.0"
multiformats "^9.9.0"
uint8arrays "3.0.0"
zod "^3.21.4"
"@atproto/lexicon@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/syntax" "^0.3.0"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.21.4"
"@atproto/syntax@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
"@atproto/xrpc@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
dependencies:
"@atproto/lexicon" "^0.4.0"
zod "^3.21.4"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3":
version "7.16.7"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz"
@@ -1455,18 +1501,6 @@
enabled "2.0.x"
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":
version "3.2.1"
resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz"
@@ -3345,72 +3379,6 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz"
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":
version "5.3.0"
resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
@@ -3901,216 +3869,6 @@
dependencies:
"@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":
version "4.1.8"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz"
@@ -4201,11 +3959,6 @@
"@types/qs" "*"
"@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":
version "4.1.5"
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz"
@@ -6337,11 +6090,6 @@ cjs-module-lexer@^1.0.0:
resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz"
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:
version "5.2.2"
resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz"
@@ -7127,68 +6875,6 @@ csstype@^3.1.1:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz"
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:
version "1.0.8"
resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz"
@@ -9299,6 +8985,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz"
integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
graphql-executor@0.0.18:
version "0.0.18"
resolved "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.18.tgz"
@@ -10236,6 +9927,11 @@ isexe@^3.1.1:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d"
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
iso-datestring-validator@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz#2daa80d2900b7a954f9f731d42f96ee0c19a6895"
integrity sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==
isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz"
@@ -11952,6 +11648,11 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
multiformats@^9.4.2, multiformats@^9.9.0:
version "9.9.0"
resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37"
integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==
multimatch@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz"
@@ -14169,18 +13870,6 @@ react@^18.2.0:
dependencies:
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:
version "2.0.0"
resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz"
@@ -15946,6 +15635,11 @@ tinyspy@^2.2.0:
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce"
integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==
tlds@^1.234.0:
version "1.252.0"
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.252.0.tgz#71d9617f4ef4cc7347843bee72428e71b8b0f419"
integrity sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz"
@@ -16195,6 +15889,13 @@ uid-number@0.0.6:
resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz"
integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
uint8arrays@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b"
integrity sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==
dependencies:
multiformats "^9.4.2"
umask@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz"
@@ -16349,11 +16050,6 @@ url-parse-lax@^3.0.0:
dependencies:
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:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
@@ -17326,9 +17022,7 @@ zen-observable@0.8.15:
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz"
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"
zod@^3.21.4:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==