Compare commits
	
		
			40 Commits
		
	
	
		
			AUT-1000
			...
			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 | 
@@ -1,262 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
import isEmpty from 'lodash/isEmpty.js';
 | 
			
		||||
import omitBy from 'lodash/omitBy.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Create post',
 | 
			
		||||
  key: 'createPost',
 | 
			
		||||
  description: 'Creates a new post.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Title',
 | 
			
		||||
      key: 'title',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Content',
 | 
			
		||||
      key: 'content',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Excerpt',
 | 
			
		||||
      key: 'excerpt',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Password',
 | 
			
		||||
      key: 'password',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: 'A password to protect access to the content and excerpt.',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Author',
 | 
			
		||||
      key: 'author',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listUsers',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Featured Media',
 | 
			
		||||
      key: 'featuredMedia',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listMedia',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Comment Status',
 | 
			
		||||
      key: 'commentStatus',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Open', value: 'open' },
 | 
			
		||||
        { label: 'Closed', value: 'closed' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Ping Status',
 | 
			
		||||
      key: 'pingStatus',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Open', value: 'open' },
 | 
			
		||||
        { label: 'Closed', value: 'closed' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Format',
 | 
			
		||||
      key: 'format',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Standard', value: 'standard' },
 | 
			
		||||
        { label: 'Aside', value: 'aside' },
 | 
			
		||||
        { label: 'Chat', value: 'chat' },
 | 
			
		||||
        { label: 'Gallery', value: 'gallery' },
 | 
			
		||||
        { label: 'Link', value: 'link' },
 | 
			
		||||
        { label: 'Image', value: 'image' },
 | 
			
		||||
        { label: 'Quote', value: 'quote' },
 | 
			
		||||
        { label: 'Status', value: 'status' },
 | 
			
		||||
        { label: 'Status', value: 'status' },
 | 
			
		||||
        { label: 'Video', value: 'video' },
 | 
			
		||||
        { label: 'Audio', value: 'audio' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Sticky',
 | 
			
		||||
      key: 'sticky',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'False', value: 'false' },
 | 
			
		||||
        { label: 'True', value: 'true' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Categories',
 | 
			
		||||
      key: 'categoryIds',
 | 
			
		||||
      type: 'dynamic',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          label: 'Category',
 | 
			
		||||
          key: 'categoryId',
 | 
			
		||||
          type: 'dropdown',
 | 
			
		||||
          required: false,
 | 
			
		||||
          variables: true,
 | 
			
		||||
          source: {
 | 
			
		||||
            type: 'query',
 | 
			
		||||
            name: 'getDynamicData',
 | 
			
		||||
            arguments: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'key',
 | 
			
		||||
                value: 'listCategories',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Tags',
 | 
			
		||||
      key: 'tagIds',
 | 
			
		||||
      type: 'dynamic',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          label: 'Tag',
 | 
			
		||||
          key: 'tagId',
 | 
			
		||||
          type: 'dropdown',
 | 
			
		||||
          required: false,
 | 
			
		||||
          variables: true,
 | 
			
		||||
          source: {
 | 
			
		||||
            type: 'query',
 | 
			
		||||
            name: 'getDynamicData',
 | 
			
		||||
            arguments: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'key',
 | 
			
		||||
                value: 'listTags',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Status',
 | 
			
		||||
      key: 'status',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listStatuses',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Date',
 | 
			
		||||
      key: 'date',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: "Post publish date in the site's timezone",
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const {
 | 
			
		||||
      title,
 | 
			
		||||
      content,
 | 
			
		||||
      excerpt,
 | 
			
		||||
      password,
 | 
			
		||||
      author,
 | 
			
		||||
      featuredMedia,
 | 
			
		||||
      commentStatus,
 | 
			
		||||
      pingStatus,
 | 
			
		||||
      format,
 | 
			
		||||
      sticky,
 | 
			
		||||
      categoryIds,
 | 
			
		||||
      tagIds,
 | 
			
		||||
      status,
 | 
			
		||||
      date,
 | 
			
		||||
    } = $.step.parameters;
 | 
			
		||||
 | 
			
		||||
    const allCategoryIds = categoryIds
 | 
			
		||||
      ?.map((categoryId) => categoryId.categoryId)
 | 
			
		||||
      .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    const allTagIds = tagIds?.map((tagId) => tagId.tagId).filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    let body = {
 | 
			
		||||
      title,
 | 
			
		||||
      content,
 | 
			
		||||
      excerpt,
 | 
			
		||||
      password,
 | 
			
		||||
      author,
 | 
			
		||||
      featured_media: featuredMedia,
 | 
			
		||||
      comment_status: commentStatus,
 | 
			
		||||
      ping_status: pingStatus,
 | 
			
		||||
      format,
 | 
			
		||||
      sticky,
 | 
			
		||||
      categories: allCategoryIds,
 | 
			
		||||
      tags: allTagIds,
 | 
			
		||||
      status,
 | 
			
		||||
      date,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    body = omitBy(body, isEmpty);
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.post('?rest_route=/wp/v2/posts', body);
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,135 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
import isEmpty from 'lodash/isEmpty.js';
 | 
			
		||||
import omitBy from 'lodash/omitBy.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Create user',
 | 
			
		||||
  key: 'createUser',
 | 
			
		||||
  description: 'Creates a new user.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Email',
 | 
			
		||||
      key: 'email',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Username',
 | 
			
		||||
      key: 'username',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Password',
 | 
			
		||||
      key: 'password',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'First Name',
 | 
			
		||||
      key: 'firstName',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Last Name',
 | 
			
		||||
      key: 'lastName',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Display Name',
 | 
			
		||||
      key: 'displayName',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Nickname',
 | 
			
		||||
      key: 'nickname',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Description',
 | 
			
		||||
      key: 'description',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Website',
 | 
			
		||||
      key: 'website',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Role',
 | 
			
		||||
      key: 'role',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Administrator', value: 'administrator' },
 | 
			
		||||
        { label: 'Author', value: 'author' },
 | 
			
		||||
        { label: 'Contributor', value: 'contributor' },
 | 
			
		||||
        { label: 'Editor', value: 'editor' },
 | 
			
		||||
        { label: 'Subscriber', value: 'subscriber' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const {
 | 
			
		||||
      email,
 | 
			
		||||
      username,
 | 
			
		||||
      password,
 | 
			
		||||
      firstName,
 | 
			
		||||
      lastName,
 | 
			
		||||
      displayName,
 | 
			
		||||
      nickname,
 | 
			
		||||
      description,
 | 
			
		||||
      website,
 | 
			
		||||
      role,
 | 
			
		||||
    } = $.step.parameters;
 | 
			
		||||
 | 
			
		||||
    let body = {
 | 
			
		||||
      email,
 | 
			
		||||
      username,
 | 
			
		||||
      password,
 | 
			
		||||
      first_name: firstName,
 | 
			
		||||
      last_name: lastName,
 | 
			
		||||
      name: displayName,
 | 
			
		||||
      nickname,
 | 
			
		||||
      description,
 | 
			
		||||
      url: website,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (role) {
 | 
			
		||||
      body.roles = [role];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    body = omitBy(body, isEmpty);
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.post('?rest_route=/wp/v2/users', body);
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Delete post',
 | 
			
		||||
  key: 'deletePost',
 | 
			
		||||
  description: 'Deletes a post.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Post ID',
 | 
			
		||||
      key: 'postId',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: 'Choose a post to delete.',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listPosts',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const { postId } = $.step.parameters;
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.delete(`?rest_route=/wp/v2/posts/${postId}`);
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Find post',
 | 
			
		||||
  key: 'findPost',
 | 
			
		||||
  description: 'Finds a post.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Post ID',
 | 
			
		||||
      key: 'postId',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: 'Choose a post to update.',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listPosts',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const { postId } = $.step.parameters;
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.get(`?rest_route=/wp/v2/posts/${postId}`);
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,35 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Find user',
 | 
			
		||||
  key: 'findUser',
 | 
			
		||||
  description: 'Finds a user.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'User ID',
 | 
			
		||||
      key: 'userId',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listUsers',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const userId = $.step.parameters.userId;
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.get(`?rest_route=/wp/v2/users/${userId}`);
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import createPost from './create-post/index.js';
 | 
			
		||||
import createUser from './create-user/index.js';
 | 
			
		||||
import deletePost from './delete-post/index.js';
 | 
			
		||||
import findPost from './find-post/index.js';
 | 
			
		||||
import findUser from './find-user/index.js';
 | 
			
		||||
import updatePost from './update-post/index.js';
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  createPost,
 | 
			
		||||
  createUser,
 | 
			
		||||
  deletePost,
 | 
			
		||||
  findPost,
 | 
			
		||||
  findUser,
 | 
			
		||||
  updatePost,
 | 
			
		||||
];
 | 
			
		||||
@@ -1,284 +0,0 @@
 | 
			
		||||
import defineAction from '../../../../helpers/define-action.js';
 | 
			
		||||
import isEmpty from 'lodash/isEmpty.js';
 | 
			
		||||
import omitBy from 'lodash/omitBy.js';
 | 
			
		||||
 | 
			
		||||
export default defineAction({
 | 
			
		||||
  name: 'Update post',
 | 
			
		||||
  key: 'updatePost',
 | 
			
		||||
  description: 'Updates a post.',
 | 
			
		||||
  arguments: [
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Post',
 | 
			
		||||
      key: 'postId',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: 'Choose a post to update.',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listPosts',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Title',
 | 
			
		||||
      key: 'title',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: true,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Content',
 | 
			
		||||
      key: 'content',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Excerpt',
 | 
			
		||||
      key: 'excerpt',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Password',
 | 
			
		||||
      key: 'password',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: 'A password to protect access to the content and excerpt.',
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Author',
 | 
			
		||||
      key: 'author',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listUsers',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Featured Media',
 | 
			
		||||
      key: 'featuredMedia',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listMedia',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Comment Status',
 | 
			
		||||
      key: 'commentStatus',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Open', value: 'open' },
 | 
			
		||||
        { label: 'Closed', value: 'closed' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Ping Status',
 | 
			
		||||
      key: 'pingStatus',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Open', value: 'open' },
 | 
			
		||||
        { label: 'Closed', value: 'closed' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Format',
 | 
			
		||||
      key: 'format',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'Standard', value: 'standard' },
 | 
			
		||||
        { label: 'Aside', value: 'aside' },
 | 
			
		||||
        { label: 'Chat', value: 'chat' },
 | 
			
		||||
        { label: 'Gallery', value: 'gallery' },
 | 
			
		||||
        { label: 'Link', value: 'link' },
 | 
			
		||||
        { label: 'Image', value: 'image' },
 | 
			
		||||
        { label: 'Quote', value: 'quote' },
 | 
			
		||||
        { label: 'Status', value: 'status' },
 | 
			
		||||
        { label: 'Status', value: 'status' },
 | 
			
		||||
        { label: 'Video', value: 'video' },
 | 
			
		||||
        { label: 'Audio', value: 'audio' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Sticky',
 | 
			
		||||
      key: 'sticky',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      options: [
 | 
			
		||||
        { label: 'False', value: 'false' },
 | 
			
		||||
        { label: 'True', value: 'true' },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Categories',
 | 
			
		||||
      key: 'categoryIds',
 | 
			
		||||
      type: 'dynamic',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          label: 'Category',
 | 
			
		||||
          key: 'categoryId',
 | 
			
		||||
          type: 'dropdown',
 | 
			
		||||
          required: false,
 | 
			
		||||
          variables: true,
 | 
			
		||||
          source: {
 | 
			
		||||
            type: 'query',
 | 
			
		||||
            name: 'getDynamicData',
 | 
			
		||||
            arguments: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'key',
 | 
			
		||||
                value: 'listCategories',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Tags',
 | 
			
		||||
      key: 'tagIds',
 | 
			
		||||
      type: 'dynamic',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      fields: [
 | 
			
		||||
        {
 | 
			
		||||
          label: 'Tag',
 | 
			
		||||
          key: 'tagId',
 | 
			
		||||
          type: 'dropdown',
 | 
			
		||||
          required: false,
 | 
			
		||||
          variables: true,
 | 
			
		||||
          source: {
 | 
			
		||||
            type: 'query',
 | 
			
		||||
            name: 'getDynamicData',
 | 
			
		||||
            arguments: [
 | 
			
		||||
              {
 | 
			
		||||
                name: 'key',
 | 
			
		||||
                value: 'listTags',
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Status',
 | 
			
		||||
      key: 'status',
 | 
			
		||||
      type: 'dropdown',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: '',
 | 
			
		||||
      variables: true,
 | 
			
		||||
      source: {
 | 
			
		||||
        type: 'query',
 | 
			
		||||
        name: 'getDynamicData',
 | 
			
		||||
        arguments: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'key',
 | 
			
		||||
            value: 'listStatuses',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      label: 'Date',
 | 
			
		||||
      key: 'date',
 | 
			
		||||
      type: 'string',
 | 
			
		||||
      required: false,
 | 
			
		||||
      description: "Post publish date in the site's timezone",
 | 
			
		||||
      variables: true,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const {
 | 
			
		||||
      postId,
 | 
			
		||||
      title,
 | 
			
		||||
      content,
 | 
			
		||||
      excerpt,
 | 
			
		||||
      password,
 | 
			
		||||
      author,
 | 
			
		||||
      featuredMedia,
 | 
			
		||||
      commentStatus,
 | 
			
		||||
      pingStatus,
 | 
			
		||||
      format,
 | 
			
		||||
      sticky,
 | 
			
		||||
      categoryIds,
 | 
			
		||||
      tagIds,
 | 
			
		||||
      status,
 | 
			
		||||
      date,
 | 
			
		||||
    } = $.step.parameters;
 | 
			
		||||
 | 
			
		||||
    const allCategoryIds = categoryIds
 | 
			
		||||
      ?.map((categoryId) => categoryId.categoryId)
 | 
			
		||||
      .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    const allTagIds = tagIds?.map((tagId) => tagId.tagId).filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    let body = {
 | 
			
		||||
      title,
 | 
			
		||||
      content,
 | 
			
		||||
      excerpt,
 | 
			
		||||
      password,
 | 
			
		||||
      author,
 | 
			
		||||
      featured_media: featuredMedia,
 | 
			
		||||
      comment_status: commentStatus,
 | 
			
		||||
      ping_status: pingStatus,
 | 
			
		||||
      format,
 | 
			
		||||
      sticky,
 | 
			
		||||
      categories: allCategoryIds,
 | 
			
		||||
      tags: allTagIds,
 | 
			
		||||
      status,
 | 
			
		||||
      date,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    body = omitBy(body, isEmpty);
 | 
			
		||||
 | 
			
		||||
    const response = await $.http.post(
 | 
			
		||||
      `?rest_route=/wp/v2/posts/${postId}`,
 | 
			
		||||
      body
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    $.setActionItem({ raw: response.data });
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
@@ -1,15 +1,3 @@
 | 
			
		||||
import listCategories from './list-categories/index.js';
 | 
			
		||||
import listMedia from './list-media/index.js';
 | 
			
		||||
import listPosts from './list-posts/index.js';
 | 
			
		||||
import listStatuses from './list-statuses/index.js';
 | 
			
		||||
import listTags from './list-tags/index.js';
 | 
			
		||||
import listUsers from './list-users/index.js';
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  listCategories,
 | 
			
		||||
  listMedia,
 | 
			
		||||
  listPosts,
 | 
			
		||||
  listStatuses,
 | 
			
		||||
  listTags,
 | 
			
		||||
  listUsers,
 | 
			
		||||
];
 | 
			
		||||
export default [listStatuses];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'List categories',
 | 
			
		||||
  key: 'listCategories',
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const categories = {
 | 
			
		||||
      data: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const params = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      per_page: 100,
 | 
			
		||||
      order: 'desc',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let totalPages = 1;
 | 
			
		||||
    do {
 | 
			
		||||
      const { data, headers } = await $.http.get(
 | 
			
		||||
        '?rest_route=/wp/v2/categories',
 | 
			
		||||
        {
 | 
			
		||||
          params,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      params.page = params.page + 1;
 | 
			
		||||
      totalPages = Number(headers['x-wp-totalpages']);
 | 
			
		||||
 | 
			
		||||
      if (data) {
 | 
			
		||||
        for (const category of data) {
 | 
			
		||||
          categories.data.push({
 | 
			
		||||
            value: category.id,
 | 
			
		||||
            name: category.name,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } while (params.page <= totalPages);
 | 
			
		||||
 | 
			
		||||
    return categories;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'List media',
 | 
			
		||||
  key: 'listMedia',
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const media = {
 | 
			
		||||
      data: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const params = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      per_page: 100,
 | 
			
		||||
      order: 'desc',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let totalPages = 1;
 | 
			
		||||
    do {
 | 
			
		||||
      const { data, headers } = await $.http.get('?rest_route=/wp/v2/media', {
 | 
			
		||||
        params,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      params.page = params.page + 1;
 | 
			
		||||
      totalPages = Number(headers['x-wp-totalpages']);
 | 
			
		||||
 | 
			
		||||
      if (data) {
 | 
			
		||||
        for (const medium of data) {
 | 
			
		||||
          media.data.push({
 | 
			
		||||
            value: medium.id,
 | 
			
		||||
            name: medium.slug,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } while (params.page <= totalPages);
 | 
			
		||||
 | 
			
		||||
    return media;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'List posts',
 | 
			
		||||
  key: 'listPosts',
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const posts = {
 | 
			
		||||
      data: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const params = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      per_page: 100,
 | 
			
		||||
      order: 'desc',
 | 
			
		||||
      status: ['publish', 'future', 'draft', 'pending', 'private'],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let totalPages = 1;
 | 
			
		||||
    do {
 | 
			
		||||
      const { data, headers } = await $.http.get('?rest_route=/wp/v2/posts', {
 | 
			
		||||
        params,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      params.page = params.page + 1;
 | 
			
		||||
      totalPages = Number(headers['x-wp-totalpages']);
 | 
			
		||||
 | 
			
		||||
      if (data) {
 | 
			
		||||
        for (const post of data) {
 | 
			
		||||
          posts.data.push({
 | 
			
		||||
            value: post.id,
 | 
			
		||||
            name: `${post.title.rendered} (${post.status})`,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } while (params.page <= totalPages);
 | 
			
		||||
 | 
			
		||||
    return posts;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'List tags',
 | 
			
		||||
  key: 'listTags',
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const tags = {
 | 
			
		||||
      data: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const params = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      per_page: 100,
 | 
			
		||||
      order: 'desc',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let totalPages = 1;
 | 
			
		||||
    do {
 | 
			
		||||
      const { data, headers } = await $.http.get('?rest_route=/wp/v2/tags', {
 | 
			
		||||
        params,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      params.page = params.page + 1;
 | 
			
		||||
      totalPages = Number(headers['x-wp-totalpages']);
 | 
			
		||||
 | 
			
		||||
      if (data) {
 | 
			
		||||
        for (const tag of data) {
 | 
			
		||||
          tags.data.push({
 | 
			
		||||
            value: tag.id,
 | 
			
		||||
            name: tag.name,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } while (params.page <= totalPages);
 | 
			
		||||
 | 
			
		||||
    return tags;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'List users',
 | 
			
		||||
  key: 'listUsers',
 | 
			
		||||
 | 
			
		||||
  async run($) {
 | 
			
		||||
    const users = {
 | 
			
		||||
      data: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const params = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
      per_page: 100,
 | 
			
		||||
      order: 'desc',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let totalPages = 1;
 | 
			
		||||
    do {
 | 
			
		||||
      const { data, headers } = await $.http.get('?rest_route=/wp/v2/users', {
 | 
			
		||||
        params,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      params.page = params.page + 1;
 | 
			
		||||
      totalPages = Number(headers['x-wp-totalpages']);
 | 
			
		||||
 | 
			
		||||
      if (data) {
 | 
			
		||||
        for (const user of data) {
 | 
			
		||||
          users.data.push({
 | 
			
		||||
            value: user.id,
 | 
			
		||||
            name: user.name,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } while (params.page <= totalPages);
 | 
			
		||||
 | 
			
		||||
    return users;
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -4,7 +4,6 @@ import setBaseUrl from './common/set-base-url.js';
 | 
			
		||||
import auth from './auth/index.js';
 | 
			
		||||
import triggers from './triggers/index.js';
 | 
			
		||||
import dynamicData from './dynamic-data/index.js';
 | 
			
		||||
import actions from './actions/index.js';
 | 
			
		||||
 | 
			
		||||
export default defineApp({
 | 
			
		||||
  name: 'WordPress',
 | 
			
		||||
@@ -19,5 +18,4 @@ export default defineApp({
 | 
			
		||||
  auth,
 | 
			
		||||
  triggers,
 | 
			
		||||
  dynamicData,
 | 
			
		||||
  actions,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ const appConfig = {
 | 
			
		||||
  isDev: appEnv === 'development',
 | 
			
		||||
  isTest: appEnv === 'test',
 | 
			
		||||
  isProd: appEnv === 'production',
 | 
			
		||||
  version: '0.11.0',
 | 
			
		||||
  version: '0.12.0',
 | 
			
		||||
  postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
 | 
			
		||||
  postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
 | 
			
		||||
  postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
 | 
			
		||||
 | 
			
		||||
    const expectedPayload = {
 | 
			
		||||
      data: {
 | 
			
		||||
        version: '0.11.0',
 | 
			
		||||
        version: '0.12.0',
 | 
			
		||||
      },
 | 
			
		||||
      meta: {
 | 
			
		||||
        count: 1,
 | 
			
		||||
 
 | 
			
		||||
@@ -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' },
 | 
			
		||||
      resetPasswordTokenSentAt: { type: 'string' },
 | 
			
		||||
      resetPasswordToken: { type: ['string', 'null'] },
 | 
			
		||||
      resetPasswordTokenSentAt: { type: ['string', 'null'], format: 'date-time' },
 | 
			
		||||
      trialExpiryDate: { type: 'string' },
 | 
			
		||||
      roleId: { type: 'string', format: 'uuid' },
 | 
			
		||||
      deletedAt: { type: 'string' },
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,7 @@ export const worker = new Worker(
 | 
			
		||||
      await user.$relatedQuery('usageData').withSoftDeleted().hardDelete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await user.$relatedQuery('accessTokens').withSoftDeleted().hardDelete();
 | 
			
		||||
    await user.$query().withSoftDeleted().hardDelete();
 | 
			
		||||
  },
 | 
			
		||||
  { connection: redisConfig }
 | 
			
		||||
 
 | 
			
		||||
@@ -518,7 +518,6 @@ export default defineConfig({
 | 
			
		||||
          collapsible: true,
 | 
			
		||||
          collapsed: true,
 | 
			
		||||
          items: [
 | 
			
		||||
            { text: 'Actions', link: '/apps/wordpress/actions' },
 | 
			
		||||
            { text: 'Triggers', link: '/apps/wordpress/triggers' },
 | 
			
		||||
            { text: 'Connection', link: '/apps/wordpress/connection' },
 | 
			
		||||
          ],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
favicon: /favicons/wordpress.svg
 | 
			
		||||
items:
 | 
			
		||||
  - name: Create post
 | 
			
		||||
    desc: Creates a new post.
 | 
			
		||||
  - name: Create user
 | 
			
		||||
    desc: Creates a new user.
 | 
			
		||||
  - name: Delete post
 | 
			
		||||
    desc: Deletes a post.
 | 
			
		||||
  - name: Find post
 | 
			
		||||
    desc: Finds a post.
 | 
			
		||||
  - name: Find user
 | 
			
		||||
    desc: Finds a user.
 | 
			
		||||
  - name: Update post
 | 
			
		||||
    desc: Updates a post.
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
  import CustomListing from '../../components/CustomListing.vue'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<CustomListing />
 | 
			
		||||
@@ -6,16 +6,12 @@ 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
    "@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",
 | 
			
		||||
@@ -32,6 +33,7 @@
 | 
			
		||||
    "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",
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,10 @@ function AccountDropdownMenu(props) {
 | 
			
		||||
AccountDropdownMenu.propTypes = {
 | 
			
		||||
  open: PropTypes.bool.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,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 +69,7 @@ function AdminApplicationSettings(props) {
 | 
			
		||||
    }),
 | 
			
		||||
    [appConfig?.data],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Form
 | 
			
		||||
      defaultValues={defaultValues}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ 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 {
 | 
			
		||||
@@ -44,34 +45,57 @@ function ContextMenu(props) {
 | 
			
		||||
      hideBackdrop={false}
 | 
			
		||||
      anchorEl={anchorEl}
 | 
			
		||||
    >
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        component={Link}
 | 
			
		||||
        to={URLS.APP_FLOWS_FOR_CONNECTION(appKey, connection.id)}
 | 
			
		||||
        onClick={createActionHandler({ type: 'viewFlows' })}
 | 
			
		||||
      >
 | 
			
		||||
        {formatMessage('connection.viewFlows')}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
 | 
			
		||||
      <MenuItem onClick={createActionHandler({ type: 'test' })}>
 | 
			
		||||
        {formatMessage('connection.testConnection')}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
 | 
			
		||||
      <MenuItem
 | 
			
		||||
        component={Link}
 | 
			
		||||
        disabled={disableReconnection}
 | 
			
		||||
        to={URLS.APP_RECONNECT_CONNECTION(
 | 
			
		||||
          appKey,
 | 
			
		||||
          connection.id,
 | 
			
		||||
          connection.appAuthClientId,
 | 
			
		||||
      <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>
 | 
			
		||||
        )}
 | 
			
		||||
        onClick={createActionHandler({ type: 'reconnect' })}
 | 
			
		||||
      >
 | 
			
		||||
        {formatMessage('connection.reconnect')}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      </Can>
 | 
			
		||||
 | 
			
		||||
      <MenuItem onClick={createActionHandler({ type: 'delete' })}>
 | 
			
		||||
        {formatMessage('connection.delete')}
 | 
			
		||||
      </MenuItem>
 | 
			
		||||
      <Can I="update" a="Connection" passThrough>
 | 
			
		||||
        {(allowed) => (
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            onClick={createActionHandler({ type: 'test' })}
 | 
			
		||||
            disabled={!allowed}
 | 
			
		||||
          >
 | 
			
		||||
            {formatMessage('connection.testConnection')}
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
        )}
 | 
			
		||||
      </Can>
 | 
			
		||||
 | 
			
		||||
      <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>
 | 
			
		||||
        )}
 | 
			
		||||
      </Can>
 | 
			
		||||
 | 
			
		||||
      <Can I="delete" a="Connection" passThrough>
 | 
			
		||||
        {(allowed) => (
 | 
			
		||||
          <MenuItem
 | 
			
		||||
            onClick={createActionHandler({ type: 'delete' })}
 | 
			
		||||
            disabled={!allowed}
 | 
			
		||||
          >
 | 
			
		||||
            {formatMessage('connection.delete')}
 | 
			
		||||
          </MenuItem>
 | 
			
		||||
        )}
 | 
			
		||||
      </Can>
 | 
			
		||||
    </Menu>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ 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';
 | 
			
		||||
@@ -16,11 +17,15 @@ function AppConnections(props) {
 | 
			
		||||
 | 
			
		||||
  if (!hasConnections) {
 | 
			
		||||
    return (
 | 
			
		||||
      <NoResultFound
 | 
			
		||||
        to={URLS.APP_ADD_CONNECTION(appKey)}
 | 
			
		||||
        text={formatMessage('app.noConnections')}
 | 
			
		||||
        data-test="connections-no-results"
 | 
			
		||||
      />
 | 
			
		||||
      <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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ 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';
 | 
			
		||||
@@ -36,11 +37,20 @@ function AppFlows(props) {
 | 
			
		||||
 | 
			
		||||
  if (!hasFlows) {
 | 
			
		||||
    return (
 | 
			
		||||
      <NoResultFound
 | 
			
		||||
        to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(appKey, connectionId)}
 | 
			
		||||
        text={formatMessage('app.noFlows')}
 | 
			
		||||
        data-test="flows-no-results"
 | 
			
		||||
      />
 | 
			
		||||
      <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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -165,6 +165,7 @@ function ChooseAppAndEventSubstep(props) {
 | 
			
		||||
            value={getOption(appOptions, step.appKey) || null}
 | 
			
		||||
            onChange={onAppChange}
 | 
			
		||||
            data-test="choose-app-autocomplete"
 | 
			
		||||
            componentsProps={{ popper: { className: 'nowheel' } }}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          {step.appKey && (
 | 
			
		||||
@@ -227,6 +228,7 @@ function ChooseAppAndEventSubstep(props) {
 | 
			
		||||
                value={getOption(actionOrTriggerOptions, step.key) || null}
 | 
			
		||||
                onChange={onEventChange}
 | 
			
		||||
                data-test="choose-event-autocomplete"
 | 
			
		||||
                componentsProps={{ popper: { className: 'nowheel' } }}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
 
 | 
			
		||||
@@ -240,6 +240,7 @@ function ChooseConnectionSubstep(props) {
 | 
			
		||||
            onChange={handleChange}
 | 
			
		||||
            loading={isAppConnectionsLoading}
 | 
			
		||||
            data-test="choose-connection-autocomplete"
 | 
			
		||||
            componentsProps={{ popper: { className: 'nowheel' } }}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <Button
 | 
			
		||||
 
 | 
			
		||||
@@ -32,9 +32,11 @@ function ControlledAutocomplete(props) {
 | 
			
		||||
    ...autocompleteProps
 | 
			
		||||
  } = props;
 | 
			
		||||
  let dependsOnValues = [];
 | 
			
		||||
 | 
			
		||||
  if (dependsOn?.length) {
 | 
			
		||||
    dependsOnValues = watch(dependsOn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const hasDependencies = dependsOnValues.length;
 | 
			
		||||
    const allDepsSatisfied = dependsOnValues.every(Boolean);
 | 
			
		||||
@@ -44,6 +46,7 @@ function ControlledAutocomplete(props) {
 | 
			
		||||
      resetField(name);
 | 
			
		||||
    }
 | 
			
		||||
  }, dependsOnValues);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Controller
 | 
			
		||||
      rules={{ required }}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,9 @@ const CustomOptions = (props) => {
 | 
			
		||||
    label,
 | 
			
		||||
    initialTabIndex,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
 | 
			
		||||
 | 
			
		||||
  React.useEffect(
 | 
			
		||||
    function applyInitialActiveTabIndex() {
 | 
			
		||||
      setActiveTabIndex((currentActiveTabIndex) => {
 | 
			
		||||
@@ -33,6 +35,7 @@ const CustomOptions = (props) => {
 | 
			
		||||
    },
 | 
			
		||||
    [initialTabIndex],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Popper
 | 
			
		||||
      open={open}
 | 
			
		||||
@@ -47,6 +50,7 @@ const CustomOptions = (props) => {
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ]}
 | 
			
		||||
      className="nowheel"
 | 
			
		||||
    >
 | 
			
		||||
      <Paper elevation={5} sx={{ width: '100%' }}>
 | 
			
		||||
        <Tabs
 | 
			
		||||
@@ -75,7 +79,10 @@ const CustomOptions = (props) => {
 | 
			
		||||
 | 
			
		||||
CustomOptions.propTypes = {
 | 
			
		||||
  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(
 | 
			
		||||
    PropTypes.shape({
 | 
			
		||||
      id: PropTypes.string.isRequired,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,7 @@ 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} />,
 | 
			
		||||
@@ -94,10 +95,14 @@ function ControlledCustomAutocomplete(props) {
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    const hasDependencies = dependsOnValues.length;
 | 
			
		||||
    if (hasDependencies) {
 | 
			
		||||
      // Reset the field when a dependent has been updated
 | 
			
		||||
      resetEditor(editor);
 | 
			
		||||
    if (mountedRef.current) {
 | 
			
		||||
      const hasDependencies = dependsOnValues.length;
 | 
			
		||||
      if (hasDependencies) {
 | 
			
		||||
        // Reset the field when a dependent has been updated
 | 
			
		||||
        resetEditor(editor);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      mountedRef.current = true;
 | 
			
		||||
    }
 | 
			
		||||
  }, dependsOnValues);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -64,11 +64,19 @@ function DynamicField(props) {
 | 
			
		||||
          <Stack
 | 
			
		||||
            direction={{ xs: 'column', sm: 'row' }}
 | 
			
		||||
            spacing={{ xs: 2 }}
 | 
			
		||||
            sx={{ display: 'flex', flex: 1 }}
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              flex: 1,
 | 
			
		||||
              minWidth: 0,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {fields.map((fieldSchema, fieldSchemaIndex) => (
 | 
			
		||||
              <Box
 | 
			
		||||
                sx={{ display: 'flex', flex: '1 0 0px' }}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  flex: '1 0 0px',
 | 
			
		||||
                  minWidth: 0,
 | 
			
		||||
                }}
 | 
			
		||||
                key={`field-${field.__id}-${fieldSchemaIndex}`}
 | 
			
		||||
              >
 | 
			
		||||
                <InputCreator
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ 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';
 | 
			
		||||
@@ -20,6 +21,9 @@ 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();
 | 
			
		||||
@@ -55,23 +59,25 @@ export default function EditorLayout() {
 | 
			
		||||
 | 
			
		||||
  const onFlowStatusUpdate = React.useCallback(
 | 
			
		||||
    async (active) => {
 | 
			
		||||
      await updateFlowStatus({
 | 
			
		||||
        variables: {
 | 
			
		||||
          input: {
 | 
			
		||||
            id: flowId,
 | 
			
		||||
            active,
 | 
			
		||||
      try {
 | 
			
		||||
        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] });
 | 
			
		||||
        await queryClient.invalidateQueries({ queryKey: ['flows', flowId] });
 | 
			
		||||
      } catch (err) {}
 | 
			
		||||
    },
 | 
			
		||||
    [flowId, queryClient],
 | 
			
		||||
  );
 | 
			
		||||
@@ -131,15 +137,28 @@ export default function EditorLayout() {
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </TopBar>
 | 
			
		||||
      <Stack direction="column" height="100%">
 | 
			
		||||
        <Container maxWidth="md">
 | 
			
		||||
          <EditorProvider value={{ readOnly: !!flow?.active }}>
 | 
			
		||||
            {!flow && !isFlowLoading && 'not found'}
 | 
			
		||||
 | 
			
		||||
            {flow && <Editor flow={flow} />}
 | 
			
		||||
          </EditorProvider>
 | 
			
		||||
        </Container>
 | 
			
		||||
      </Stack>
 | 
			
		||||
      {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>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <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 } },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await queryClient.invalidateQueries({
 | 
			
		||||
      queryKey: ['apps', appKey, 'flows'],
 | 
			
		||||
    });
 | 
			
		||||
    if (appKey) {
 | 
			
		||||
      await queryClient.invalidateQueries({
 | 
			
		||||
        queryKey: ['apps', appKey, 'flows'],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), {
 | 
			
		||||
      variant: 'success',
 | 
			
		||||
      SnackbarProps: {
 | 
			
		||||
@@ -56,9 +59,12 @@ function ContextMenu(props) {
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await queryClient.invalidateQueries({
 | 
			
		||||
      queryKey: ['apps', appKey, 'flows'],
 | 
			
		||||
    });
 | 
			
		||||
    if (appKey) {
 | 
			
		||||
      await queryClient.invalidateQueries({
 | 
			
		||||
        queryKey: ['apps', appKey, 'flows'],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enqueueSnackbar(formatMessage('flow.successfullyDeleted'), {
 | 
			
		||||
      variant: 'success',
 | 
			
		||||
    });
 | 
			
		||||
@@ -110,7 +116,7 @@ ContextMenu.propTypes = {
 | 
			
		||||
  ]).isRequired,
 | 
			
		||||
  onDeleteFlow: PropTypes.func,
 | 
			
		||||
  onDuplicateFlow: PropTypes.func,
 | 
			
		||||
  appKey: PropTypes.string.isRequired,
 | 
			
		||||
  appKey: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ContextMenu;
 | 
			
		||||
 
 | 
			
		||||
@@ -38,20 +38,24 @@ 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">
 | 
			
		||||
@@ -127,7 +131,7 @@ FlowRow.propTypes = {
 | 
			
		||||
  flow: FlowPropType.isRequired,
 | 
			
		||||
  onDeleteFlow: PropTypes.func,
 | 
			
		||||
  onDuplicateFlow: PropTypes.func,
 | 
			
		||||
  appKey: PropTypes.string.isRequired,
 | 
			
		||||
  appKey: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default FlowRow;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,6 @@ 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';
 | 
			
		||||
@@ -33,77 +30,18 @@ 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);
 | 
			
		||||
@@ -114,6 +52,10 @@ 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) {
 | 
			
		||||
@@ -168,6 +110,12 @@ function FlowStep(props) {
 | 
			
		||||
      ? triggerSubstepsData
 | 
			
		||||
      : actionSubstepsData || [];
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (!isEqual(substeps, formResolverContext.substeps)) {
 | 
			
		||||
      setFormResolverContext({ substeps, additionalFields: {} });
 | 
			
		||||
    }
 | 
			
		||||
  }, [substeps]);
 | 
			
		||||
 | 
			
		||||
  const handleChange = React.useCallback(({ step }) => {
 | 
			
		||||
    onChange(step);
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -180,11 +128,6 @@ function FlowStep(props) {
 | 
			
		||||
    handleChange({ step: val });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const stepValidationSchema = React.useMemo(
 | 
			
		||||
    () => generateValidationSchema(substeps),
 | 
			
		||||
    [substeps],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (!apps?.data) {
 | 
			
		||||
    return (
 | 
			
		||||
      <CircularProgress
 | 
			
		||||
@@ -213,6 +156,15 @@ 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;
 | 
			
		||||
 | 
			
		||||
@@ -266,7 +218,8 @@ function FlowStep(props) {
 | 
			
		||||
              <Form
 | 
			
		||||
                defaultValues={step}
 | 
			
		||||
                onSubmit={handleSubmit}
 | 
			
		||||
                resolver={stepValidationSchema}
 | 
			
		||||
                resolver={validationSchemaResolver}
 | 
			
		||||
                context={formResolverContext}
 | 
			
		||||
              >
 | 
			
		||||
                <ChooseAppAndEventSubstep
 | 
			
		||||
                  expanded={currentSubstep === 0}
 | 
			
		||||
@@ -330,6 +283,9 @@ function FlowStep(props) {
 | 
			
		||||
                            onSubmit={expandNextStep}
 | 
			
		||||
                            onChange={handleChange}
 | 
			
		||||
                            step={step}
 | 
			
		||||
                            addAdditionalFieldsValidation={
 | 
			
		||||
                              addAdditionalFieldsValidation
 | 
			
		||||
                            }
 | 
			
		||||
                          />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </React.Fragment>
 | 
			
		||||
@@ -360,7 +316,6 @@ function FlowStep(props) {
 | 
			
		||||
FlowStep.propTypes = {
 | 
			
		||||
  collapsed: PropTypes.bool,
 | 
			
		||||
  step: StepPropType.isRequired,
 | 
			
		||||
  index: PropTypes.number,
 | 
			
		||||
  onOpen: PropTypes.func,
 | 
			
		||||
  onClose: PropTypes.func,
 | 
			
		||||
  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 = {
 | 
			
		||||
  stepId: PropTypes.string.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,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,9 @@ function FlowSubstep(props) {
 | 
			
		||||
    onCollapse,
 | 
			
		||||
    onSubmit,
 | 
			
		||||
    step,
 | 
			
		||||
    addAdditionalFieldsValidation,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const { name, arguments: args } = substep;
 | 
			
		||||
  const editorContext = React.useContext(EditorContext);
 | 
			
		||||
  const formContext = useFormContext();
 | 
			
		||||
@@ -54,6 +56,7 @@ function FlowSubstep(props) {
 | 
			
		||||
                  stepId={step.id}
 | 
			
		||||
                  disabled={editorContext.readOnly}
 | 
			
		||||
                  showOptionValue={true}
 | 
			
		||||
                  addAdditionalFieldsValidation={addAdditionalFieldsValidation}
 | 
			
		||||
                />
 | 
			
		||||
              ))}
 | 
			
		||||
            </Stack>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
 | 
			
		||||
 | 
			
		||||
const noop = () => null;
 | 
			
		||||
 | 
			
		||||
export default function Form(props) {
 | 
			
		||||
  const {
 | 
			
		||||
    children,
 | 
			
		||||
@@ -9,24 +11,31 @@ 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}>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,9 @@ export default function InputCreator(props) {
 | 
			
		||||
    disabled,
 | 
			
		||||
    showOptionValue,
 | 
			
		||||
    shouldUnregister,
 | 
			
		||||
    addAdditionalFieldsValidation,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    key: name,
 | 
			
		||||
    label,
 | 
			
		||||
@@ -33,6 +35,7 @@ export default function InputCreator(props) {
 | 
			
		||||
    description,
 | 
			
		||||
    type,
 | 
			
		||||
  } = schema;
 | 
			
		||||
 | 
			
		||||
  const { data, loading } = useDynamicData(stepId, schema);
 | 
			
		||||
  const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
 | 
			
		||||
    useDynamicFields(stepId, schema);
 | 
			
		||||
@@ -40,6 +43,10 @@ export default function InputCreator(props) {
 | 
			
		||||
 | 
			
		||||
  const computedName = namePrefix ? `${namePrefix}.${name}` : name;
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    addAdditionalFieldsValidation?.({ [name]: additionalFields });
 | 
			
		||||
  }, [additionalFields]);
 | 
			
		||||
 | 
			
		||||
  if (type === 'dynamic') {
 | 
			
		||||
    return (
 | 
			
		||||
      <DynamicField
 | 
			
		||||
@@ -80,6 +87,7 @@ export default function InputCreator(props) {
 | 
			
		||||
            disabled={disabled}
 | 
			
		||||
            showOptionValue={showOptionValue}
 | 
			
		||||
            shouldUnregister={shouldUnregister}
 | 
			
		||||
            componentsProps={{ popper: { className: 'nowheel' } }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,10 @@ 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) {
 | 
			
		||||
@@ -15,12 +17,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>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ 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 {
 | 
			
		||||
@@ -31,33 +32,41 @@ 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 }}
 | 
			
		||||
@@ -127,6 +136,7 @@ const PowerInput = (props) => {
 | 
			
		||||
                anchorEl={editorRef.current}
 | 
			
		||||
                data={stepsWithVariables}
 | 
			
		||||
                onSuggestionClick={handleVariableSuggestionClick}
 | 
			
		||||
                className="nowheel"
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <FormHelperText variant="outlined">{description}</FormHelperText>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ 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 = {};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 { Settings } from 'luxon';
 | 
			
		||||
 | 
			
		||||
import ThemeProvider from 'components/ThemeProvider';
 | 
			
		||||
import IntlProvider from 'components/IntlProvider';
 | 
			
		||||
import ApolloProvider from 'components/ApolloProvider';
 | 
			
		||||
@@ -10,6 +12,9 @@ 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);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ 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;
 | 
			
		||||
@@ -92,7 +93,7 @@ export default function Application() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return options;
 | 
			
		||||
  }, [appKey, appConfig?.data, currentUserAbility]);
 | 
			
		||||
  }, [appKey, appConfig?.data, currentUserAbility, formatMessage]);
 | 
			
		||||
 | 
			
		||||
  if (loading) return null;
 | 
			
		||||
 | 
			
		||||
@@ -118,37 +119,46 @@ export default function Application() {
 | 
			
		||||
                <Route
 | 
			
		||||
                  path={`${URLS.FLOWS}/*`}
 | 
			
		||||
                  element={
 | 
			
		||||
                    <ConditionalIconButton
 | 
			
		||||
                      type="submit"
 | 
			
		||||
                      variant="contained"
 | 
			
		||||
                      color="primary"
 | 
			
		||||
                      size="large"
 | 
			
		||||
                      component={Link}
 | 
			
		||||
                      to={URLS.CREATE_FLOW_WITH_APP_AND_CONNECTION(
 | 
			
		||||
                        appKey,
 | 
			
		||||
                        connectionId,
 | 
			
		||||
                    <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>
 | 
			
		||||
                      )}
 | 
			
		||||
                      fullWidth
 | 
			
		||||
                      icon={<AddIcon />}
 | 
			
		||||
                      disabled={!currentUserAbility.can('create', 'Flow')}
 | 
			
		||||
                    >
 | 
			
		||||
                      {formatMessage('app.createFlow')}
 | 
			
		||||
                    </ConditionalIconButton>
 | 
			
		||||
                    </Can>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Route
 | 
			
		||||
                  path={`${URLS.CONNECTIONS}/*`}
 | 
			
		||||
                  element={
 | 
			
		||||
                    <SplitButton
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        (appConfig?.data &&
 | 
			
		||||
                          !appConfig?.data?.canConnect &&
 | 
			
		||||
                          !appConfig?.data?.canCustomConnect) ||
 | 
			
		||||
                        connectionOptions.every(({ disabled }) => disabled)
 | 
			
		||||
                      }
 | 
			
		||||
                      options={connectionOptions}
 | 
			
		||||
                    />
 | 
			
		||||
                    <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>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
              </Routes>
 | 
			
		||||
@@ -169,17 +179,20 @@ export default function Application() {
 | 
			
		||||
                    label={formatMessage('app.connections')}
 | 
			
		||||
                    to={URLS.APP_CONNECTIONS(appKey)}
 | 
			
		||||
                    value={URLS.APP_CONNECTIONS_PATTERN}
 | 
			
		||||
                    disabled={!app.supportsConnections}
 | 
			
		||||
                    disabled={
 | 
			
		||||
                      !currentUserAbility.can('read', 'Connection') ||
 | 
			
		||||
                      !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>
 | 
			
		||||
@@ -187,14 +200,20 @@ export default function Application() {
 | 
			
		||||
              <Routes>
 | 
			
		||||
                <Route
 | 
			
		||||
                  path={`${URLS.FLOWS}/*`}
 | 
			
		||||
                  element={<AppFlows appKey={appKey} />}
 | 
			
		||||
                  element={
 | 
			
		||||
                    <Can I="read" a="Flow">
 | 
			
		||||
                      <AppFlows appKey={appKey} />
 | 
			
		||||
                    </Can>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Route
 | 
			
		||||
                  path={`${URLS.CONNECTIONS}/*`}
 | 
			
		||||
                  element={<AppConnections appKey={appKey} />}
 | 
			
		||||
                  element={
 | 
			
		||||
                    <Can I="read" a="Connection">
 | 
			
		||||
                      <AppConnections appKey={appKey} />
 | 
			
		||||
                    </Can>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <Route
 | 
			
		||||
                  path="/"
 | 
			
		||||
                  element={
 | 
			
		||||
@@ -218,17 +237,24 @@ export default function Application() {
 | 
			
		||||
        <Route
 | 
			
		||||
          path="/connections/add"
 | 
			
		||||
          element={
 | 
			
		||||
            <AddAppConnection onClose={goToApplicationPage} application={app} />
 | 
			
		||||
            <Can I="create" a="Connection">
 | 
			
		||||
              <AddAppConnection
 | 
			
		||||
                onClose={goToApplicationPage}
 | 
			
		||||
                application={app}
 | 
			
		||||
              />
 | 
			
		||||
            </Can>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Route
 | 
			
		||||
          path="/connections/:connectionId/reconnect"
 | 
			
		||||
          element={
 | 
			
		||||
            <ReconnectConnection
 | 
			
		||||
              application={app}
 | 
			
		||||
              onClose={goToApplicationPage}
 | 
			
		||||
            />
 | 
			
		||||
            <Can I="create" a="Connection">
 | 
			
		||||
              <ReconnectConnection
 | 
			
		||||
                application={app}
 | 
			
		||||
                onClose={goToApplicationPage}
 | 
			
		||||
              />
 | 
			
		||||
            </Can>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </Routes>
 | 
			
		||||
 
 | 
			
		||||
@@ -84,10 +84,14 @@ export default function Applications() {
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {!isLoading && !hasApps && (
 | 
			
		||||
          <NoResultFound
 | 
			
		||||
            text={formatMessage('apps.noConnections')}
 | 
			
		||||
            to={URLS.NEW_APP_CONNECTION}
 | 
			
		||||
          />
 | 
			
		||||
          <Can I="create" a="Connection" passThrough>
 | 
			
		||||
            {(allowed) => (
 | 
			
		||||
              <NoResultFound
 | 
			
		||||
                text={formatMessage('apps.noConnections')}
 | 
			
		||||
                {...(allowed && { to: URLS.NEW_APP_CONNECTION })}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Can>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {!isLoading &&
 | 
			
		||||
 
 | 
			
		||||
@@ -7,13 +7,15 @@ 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] = useMutation(CREATE_FLOW);
 | 
			
		||||
  const [createFlow, { error }] = useMutation(CREATE_FLOW);
 | 
			
		||||
  const appKey = searchParams.get('appKey');
 | 
			
		||||
  const connectionId = searchParams.get('connectionId');
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    async function initiate() {
 | 
			
		||||
      const variables = {};
 | 
			
		||||
@@ -33,6 +35,11 @@ export default function CreateFlow() {
 | 
			
		||||
    }
 | 
			
		||||
    initiate();
 | 
			
		||||
  }, [createFlow, navigate, appKey, connectionId]);
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
@@ -45,7 +52,6 @@ export default function CreateFlow() {
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <CircularProgress size={16} thickness={7.5} />
 | 
			
		||||
 | 
			
		||||
      <Typography variant="body2">
 | 
			
		||||
        {formatMessage('createFlow.creating')}
 | 
			
		||||
      </Typography>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ 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';
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +27,7 @@ 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 },
 | 
			
		||||
@@ -124,7 +126,9 @@ export default function Flows() {
 | 
			
		||||
        {!isLoading && !hasFlows && (
 | 
			
		||||
          <NoResultFound
 | 
			
		||||
            text={formatMessage('flows.noFlows')}
 | 
			
		||||
            to={URLS.CREATE_FLOW}
 | 
			
		||||
            {...(currentUserAbility.can('create', 'Flow') && {
 | 
			
		||||
              to: URLS.CREATE_FLOW,
 | 
			
		||||
            })}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {!isLoading && pageInfo && pageInfo.totalPages > 1 && (
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ services:
 | 
			
		||||
    name: automatisch-main
 | 
			
		||||
    env: docker
 | 
			
		||||
    dockerfilePath: ./docker/Dockerfile
 | 
			
		||||
    dockerContext: ./docker
 | 
			
		||||
    dockerContext: .
 | 
			
		||||
    repo: https://github.com/automatisch/automatisch
 | 
			
		||||
    autoDeploy: false
 | 
			
		||||
    envVars:
 | 
			
		||||
@@ -47,7 +47,7 @@ services:
 | 
			
		||||
    name: automatisch-worker
 | 
			
		||||
    env: docker
 | 
			
		||||
    dockerfilePath: ./docker/Dockerfile
 | 
			
		||||
    dockerContext: ./docker
 | 
			
		||||
    dockerContext: .
 | 
			
		||||
    repo: https://github.com/automatisch/automatisch
 | 
			
		||||
    autoDeploy: false
 | 
			
		||||
    envVars:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										384
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										384
									
								
								yarn.lock
									
									
									
									
									
								
							@@ -1455,6 +1455,18 @@
 | 
			
		||||
    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"
 | 
			
		||||
@@ -3333,6 +3345,72 @@
 | 
			
		||||
  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"
 | 
			
		||||
@@ -3823,6 +3901,216 @@
 | 
			
		||||
  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"
 | 
			
		||||
@@ -3913,6 +4201,11 @@
 | 
			
		||||
    "@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"
 | 
			
		||||
@@ -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"
 | 
			
		||||
  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"
 | 
			
		||||
@@ -6829,6 +7127,68 @@ 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"
 | 
			
		||||
@@ -13809,6 +14169,18 @@ 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"
 | 
			
		||||
@@ -15977,6 +16349,11 @@ 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"
 | 
			
		||||
@@ -16948,3 +17325,10 @@ zen-observable@0.8.15:
 | 
			
		||||
  version "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"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user