Skip to content

Commit d57fe60

Browse files
authored
Merge pull request #106 from FlowTestAI/cloning-flow
feat: ability to clone a flow
2 parents f4129b9 + 19c52aa commit d57fe60

17 files changed

Lines changed: 314 additions & 84 deletions

File tree

.changeset/weak-ties-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"flowtestai": minor
3+
---
4+
5+
ability to clone a fow and expand output node for bigger view

packages/flowtest-electron/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ contextBridge.exposeInMainWorld('ipcRenderer', {
66
on: (channel, handler) => ipcRenderer.on(channel, (event, ...args) => handler(...args)),
77
join: (...args) => path.join(...args),
88
relative: (...args) => path.relative(...args),
9+
dirname: (...args) => path.dirname(...args),
910
});

packages/flowtest-electron/src/ipc/collection.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ const registerRendererEventHandlers = (mainWindow, watcher) => {
237237
}
238238
});
239239

240+
ipcMain.handle('renderer:clone-flowtest', async (event, name, flowtestPath) => {
241+
try {
242+
const content = readFile(flowtestPath);
243+
createFile(`${name}.flow`, path.dirname(flowtestPath), content);
244+
console.log(`Cloned file: ${name}.flow`);
245+
} catch (error) {
246+
return Promise.reject(error);
247+
}
248+
});
249+
240250
ipcMain.handle('renderer:read-flowtest', async (event, pathname, collectionId) => {
241251
try {
242252
const content = readFile(pathname);

src/components/atoms/Editor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,5 @@ export const Editor = ({ ...props }) => {
4747
};
4848
}, []);
4949

50-
return <div ref={editor} className='cm-editor cm-scroller'></div>;
50+
return <div ref={editor} className={`${props.classes} overflow-auto`}></div>;
5151
};

src/components/atoms/sidebar/collections/OptionsMenu.js

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -36,44 +36,67 @@ const OptionsMenu = ({ collectionId, directory, itemType }) => {
3636
data-click-from='options-menu'
3737
data-item-type={itemType}
3838
>
39-
<div className='px-1 py-1' data-click-from='options-menu' data-item-type={itemType}>
40-
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
41-
<button
42-
className={menuItemsStyles}
43-
data-click-from='options-menu'
44-
data-options-menu-item={DirectoryOptionsActions.addNewFolder.value}
45-
data-path-name={directory.pathname}
46-
data-item-type={itemType}
47-
data-collection-id={collectionId}
48-
>
49-
<FolderPlusIcon
50-
className='w-4 h-4 mr-2'
51-
aria-hidden='true'
39+
{itemType === OBJ_TYPES.collection || itemType === OBJ_TYPES.folder ? (
40+
<div className='px-1 py-1' data-click-from='options-menu' data-item-type={itemType}>
41+
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
42+
<button
43+
className={menuItemsStyles}
5244
data-click-from='options-menu'
45+
data-options-menu-item={DirectoryOptionsActions.addNewFolder.value}
46+
data-path-name={directory.pathname}
5347
data-item-type={itemType}
54-
/>
55-
{DirectoryOptionsActions.addNewFolder.displayValue}
56-
</button>
57-
</Menu.Item>
58-
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
59-
<button
60-
className={menuItemsStyles}
61-
data-click-from='options-menu'
62-
data-options-menu-item={DirectoryOptionsActions.addNewFlow.value}
63-
data-path-name={directory.pathname}
64-
data-item-type={itemType}
65-
data-collection-id={collectionId}
66-
>
67-
<PencilSquareIcon
68-
className='w-4 h-4 mr-2'
69-
aria-hidden='true'
48+
data-collection-id={collectionId}
49+
>
50+
<FolderPlusIcon
51+
className='w-4 h-4 mr-2'
52+
aria-hidden='true'
53+
data-click-from='options-menu'
54+
data-item-type={itemType}
55+
/>
56+
{DirectoryOptionsActions.addNewFolder.displayValue}
57+
</button>
58+
</Menu.Item>
59+
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
60+
<button
61+
className={menuItemsStyles}
7062
data-click-from='options-menu'
63+
data-options-menu-item={DirectoryOptionsActions.addNewFlow.value}
64+
data-path-name={directory.pathname}
7165
data-item-type={itemType}
72-
/>
73-
{DirectoryOptionsActions.addNewFlow.displayValue}
74-
</button>
75-
</Menu.Item>
76-
</div>
66+
data-collection-id={collectionId}
67+
>
68+
<PencilSquareIcon
69+
className='w-4 h-4 mr-2'
70+
aria-hidden='true'
71+
data-click-from='options-menu'
72+
data-item-type={itemType}
73+
/>
74+
{DirectoryOptionsActions.addNewFlow.displayValue}
75+
</button>
76+
</Menu.Item>
77+
</div>
78+
) : (
79+
<div className='px-1 py-1' data-click-from='options-menu' data-item-type={itemType}>
80+
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
81+
<button
82+
className={menuItemsStyles}
83+
data-click-from='options-menu'
84+
data-options-menu-item={DirectoryOptionsActions.cloneFlow.value}
85+
data-path-name={directory.pathname}
86+
data-item-type={itemType}
87+
data-collection-id={collectionId}
88+
>
89+
<PencilSquareIcon
90+
className='w-4 h-4 mr-2'
91+
aria-hidden='true'
92+
data-click-from='options-menu'
93+
data-item-type={itemType}
94+
/>
95+
{DirectoryOptionsActions.cloneFlow.displayValue}
96+
</button>
97+
</Menu.Item>
98+
</div>
99+
)}
77100
<div className='px-1 py-1' data-click-from='options-menu' data-item-type={itemType}>
78101
<Menu.Item data-click-from='options-menu' data-item-type={itemType}>
79102
<button

src/components/molecules/flow/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ const Flow = ({ tab, collectionId }) => {
226226
onMoveEnd={(event, data) => {
227227
setViewport(data);
228228
}}
229+
minZoom={0}
230+
maxZoom={2}
229231
isValidConnection={isValidConnection}
230232
>
231233
<Background variant='dots' gap={12} size={1} />

src/components/molecules/flow/nodes/OutputNode.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import * as React from 'react';
22
import { PropTypes } from 'prop-types';
33
import FlowNode from 'components/atoms/flow/FlowNode';
44
import { Editor } from 'components/atoms/Editor';
5+
import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
6+
import OuputNodeExpandedModal from 'components/molecules/modals/OutputNodeExpandedModal';
7+
import Tippy from '@tippyjs/react';
8+
import 'tippy.js/dist/tippy.css';
59

610
const OutputNode = ({ id, data }) => {
11+
const [outputExpandedModal, setOutputExpandedModal] = React.useState(false);
12+
713
return (
814
<FlowNode
915
title='Output'
@@ -12,13 +18,32 @@ const OutputNode = ({ id, data }) => {
1218
handleRight={true}
1319
handleRightData={{ type: 'source' }}
1420
>
21+
{data.output ? (
22+
<button type='button' onClick={() => setOutputExpandedModal(true)}>
23+
<Tippy content='Expand' placement='top'>
24+
<ArrowsPointingOutIcon className='w-5 h-5' />
25+
</Tippy>
26+
</button>
27+
) : (
28+
<></>
29+
)}
1530
<div className='w-full text-xs text-gray-900 border border-gray-300 rounded-lg nodrag nowheel min-w-72 bg-gray-50 outline-blue-300 focus:border-blue-100 focus:ring-blue-100'>
1631
{data.output ? (
17-
<Editor name='output-text' value={JSON.stringify(data.output, null, 2)} readOnly={true} />
32+
<Editor
33+
name='output-text'
34+
value={JSON.stringify(data.output, null, 2)}
35+
readOnly={true}
36+
classes={'w-96 h-96'}
37+
/>
1838
) : (
1939
<div className='p-2'>{'Run flow to see data'}</div>
2040
)}
2141
</div>
42+
<OuputNodeExpandedModal
43+
closeFn={() => setOutputExpandedModal(false)}
44+
open={outputExpandedModal}
45+
data={data.output}
46+
/>
2247
</FlowNode>
2348
);
2449
};

src/components/molecules/flow/nodes/RequestBody.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const RequestBody = ({ nodeId, nodeData }) => {
115115
{requestBodyTypeOptions.map((bodyTypeOption, index) => (
116116
<Menu.Item key={index} data-click-from='body-type-menu' onClick={() => handleClose(bodyTypeOption)}>
117117
<button
118-
className='flex items-center w-full px-2 py-2 text-sm text-gray-900 rounded-md group hover:bg-background-light'
118+
className='flex items-center w-full px-2 py-2 text-sm text-gray-900 rounded-md hover:bg-background-light group'
119119
data-click-from='body-type-menu'
120120
>
121121
{bodyTypeOption}
@@ -132,7 +132,12 @@ const RequestBody = ({ nodeId, nodeData }) => {
132132
<div className='p-4 bg-background'>
133133
<div className='w-full nodrag nowheel min-w-72'>
134134
<div className='bg-background-lighter'>
135-
<Editor name='request-body-json' onChange={(e) => handleRawJson(e)} value={nodeData.requestBody.body} />
135+
<Editor
136+
name='request-body-json'
137+
onChange={(e) => handleRawJson(e)}
138+
value={nodeData.requestBody.body}
139+
classes={'w-96 h-96'}
140+
/>
136141
</div>
137142
<Button
138143
btnType={BUTTON_TYPES.secondary}

src/components/molecules/flow/nodes/RequestNode.js

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { useEffect, useState, Fragment } from 'react';
22
import { PropTypes } from 'prop-types';
33
import RequestBody from './RequestBody';
44
import FlowNode from 'components/atoms/flow/FlowNode';
@@ -9,9 +9,12 @@ import AddVariableModal from 'components/molecules/modals/flow/AddVariableModal'
99
import useCanvasStore from 'stores/CanvasStore';
1010
import TextInput from 'components/atoms/common/TextInput';
1111
import NodeHorizontalDivider from 'components/atoms/flow/NodeHorizontalDivider';
12+
import { Listbox, Transition } from '@headlessui/react';
13+
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
1214

1315
const RequestNode = ({ id, data }) => {
1416
const setRequestNodeUrl = useCanvasStore((state) => state.setRequestNodeUrl);
17+
const setRequestNodeType = useCanvasStore((state) => state.setRequestNodeType);
1518
const requestNodeAddPreRequestVar = useCanvasStore((state) => state.requestNodeAddPreRequestVar);
1619
const requestNodeDeletePreRequestVar = useCanvasStore((state) => state.requestNodeDeletePreRequestVar);
1720
const requestNodeChangePreRequestVar = useCanvasStore((state) => state.requestNodeChangePreRequestVar);
@@ -100,6 +103,8 @@ const RequestNode = ({ id, data }) => {
100103
);
101104
};
102105

106+
const requestTypes = ['GET', 'PUT', 'POST', 'DELETE'];
107+
103108
return (
104109
<FlowNode
105110
title={data.requestType + ' Request'}
@@ -108,12 +113,58 @@ const RequestNode = ({ id, data }) => {
108113
handleRight={true}
109114
handleRightData={{ type: 'source' }}
110115
>
111-
<div className='min-w-60'>
112-
<div className='pb-4'>
116+
<div className='min-w-80'>
117+
<div className='flex items-center justify-center gap-2 py-4'>
118+
<Listbox
119+
value={data.requestType}
120+
onChange={(selectedValue) => {
121+
setRequestNodeType(id, selectedValue);
122+
}}
123+
>
124+
<div className='relative w-36'>
125+
<Listbox.Button className='relative w-full p-2 text-left border rounded cursor-default border-cyan-950'>
126+
<span className='block truncate'>{data.requestType}</span>
127+
<span className='absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none'>
128+
<ChevronUpDownIcon className='w-5 h-5' aria-hidden='true' />
129+
</span>
130+
</Listbox.Button>
131+
<Transition
132+
as={Fragment}
133+
leave='transition ease-in duration-100'
134+
leaveFrom='opacity-100'
135+
leaveTo='opacity-0'
136+
>
137+
<Listbox.Options className='absolute z-50 w-full py-1 mt-1 overflow-auto text-base bg-white max-h-60 focus:outline-none'>
138+
{requestTypes.map((reqType) => (
139+
<Listbox.Option
140+
key={reqType}
141+
className={({ active }) =>
142+
`relative cursor-default select-none py-2 pl-7 pr-4 hover:font-semibold ${
143+
active ? 'bg-background-light text-slate-900' : ''
144+
}`
145+
}
146+
value={reqType}
147+
>
148+
{({ selected }) => (
149+
<>
150+
<span className={`block`}>{reqType}</span>
151+
{selected ? (
152+
<span className='absolute inset-y-0 left-0 flex items-center pl-1 font-semibold'>
153+
<CheckIcon className='w-5 h-5' aria-hidden='true' />
154+
</span>
155+
) : null}
156+
</>
157+
)}
158+
</Listbox.Option>
159+
))}
160+
</Listbox.Options>
161+
</Transition>
162+
</div>
163+
</Listbox>
113164
<TextInput
114165
placeHolder={`Enter URL for a ${data.requestType} request`}
115166
onChangeHandler={handleUrlInputChange}
116-
name={'username'}
167+
name={'url'}
117168
value={data.url ? data.url : ''}
118169
/>
119170
</div>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React, { Fragment, useState } from 'react';
2+
import { PropTypes } from 'prop-types';
3+
import { Dialog, Transition, Listbox } from '@headlessui/react';
4+
import Button from 'components/atoms/common/Button';
5+
import { BUTTON_INTENT_TYPES, BUTTON_TYPES, GENAI_MODELS } from 'constants/Common';
6+
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';
7+
import { Square3Stack3DIcon } from '@heroicons/react/24/outline';
8+
import { generateFlowData } from '../flow/flowtestai';
9+
import { init } from '../flow';
10+
import useCanvasStore from 'stores/CanvasStore';
11+
import { toast } from 'react-toastify';
12+
import { isEqual } from 'lodash';
13+
import useCommonStore from 'stores/CommonStore';
14+
import useCollectionStore from 'stores/CollectionStore';
15+
import { promiseWithTimeout } from 'utils/common';
16+
import { Editor } from 'components/atoms/Editor';
17+
18+
const OuputNodeExpandedModal = ({ closeFn = () => null, open = false, data }) => {
19+
return (
20+
<Transition appear show={open} as={Fragment}>
21+
<Dialog as='div' className='relative z-10' onClose={closeFn}>
22+
<Transition.Child
23+
as={Fragment}
24+
enter='ease-out duration-300'
25+
enterFrom='opacity-0'
26+
enterTo='opacity-100'
27+
leave='ease-in duration-200'
28+
leaveFrom='opacity-100'
29+
leaveTo='opacity-0'
30+
>
31+
<div className='fixed inset-0 bg-black/25' />
32+
</Transition.Child>
33+
34+
<div className='fixed inset-0 overflow-y-auto'>
35+
<div className='flex items-center justify-center min-h-full p-4 text-center'>
36+
<Transition.Child
37+
as={Fragment}
38+
enter='ease-out duration-300'
39+
enterFrom='opacity-0 scale-95'
40+
enterTo='opacity-100 scale-100'
41+
leave='ease-in duration-200'
42+
leaveFrom='opacity-100 scale-100'
43+
leaveTo='opacity-0 scale-95'
44+
>
45+
<Dialog.Panel className='w-full max-w-2xl p-6 overflow-hidden text-left align-middle transition-all transform bg-white rounded shadow-xl'>
46+
<Dialog.Title as='h3' className='pb-4 text-lg font-semibold text-center border-b border-gray-300'>
47+
Ouput
48+
</Dialog.Title>
49+
<div className='mt-6'>
50+
<Editor
51+
name='output-text'
52+
value={JSON.stringify(data, null, 2)}
53+
readOnly={true}
54+
classes={'w-1/2 h-1/2'}
55+
/>
56+
</div>
57+
</Dialog.Panel>
58+
</Transition.Child>
59+
</div>
60+
</div>
61+
</Dialog>
62+
</Transition>
63+
);
64+
};
65+
66+
OuputNodeExpandedModal.propTypes = {
67+
closeFn: PropTypes.func.isRequired,
68+
open: PropTypes.boolean.isRequired,
69+
};
70+
export default OuputNodeExpandedModal;

0 commit comments

Comments
 (0)