feat: add PowerInput component
This commit is contained in:

committed by
Ömer Faruk Aydın

parent
c864a1062d
commit
d06f21c927
@@ -12,9 +12,7 @@ export default class CreateTweet {
|
|||||||
accessSecret: connectionData.accessSecret,
|
accessSecret: connectionData.accessSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (parameters) {
|
this.parameters = parameters;
|
||||||
this.parameters = JSON.parse(parameters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
@@ -277,7 +277,9 @@
|
|||||||
"label": "Tweet body",
|
"label": "Tweet body",
|
||||||
"key": "tweet",
|
"key": "tweet",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": true
|
"required": true,
|
||||||
|
"description": "The content of your new tweet.",
|
||||||
|
"variables": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@@ -54,7 +54,6 @@ const createStepResolver = async (
|
|||||||
|
|
||||||
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
|
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
|
||||||
await nextStep.$query().patchAndFetch({
|
await nextStep.$query().patchAndFetch({
|
||||||
...nextStep,
|
|
||||||
position: step.position + index + 1,
|
position: step.position + index + 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -26,7 +26,6 @@ const deleteStepResolver = async (
|
|||||||
|
|
||||||
const nextStepQueries = nextSteps.map(async (nextStep) => {
|
const nextStepQueries = nextSteps.map(async (nextStep) => {
|
||||||
await nextStep.$query().patch({
|
await nextStep.$query().patch({
|
||||||
...nextStep,
|
|
||||||
position: nextStep.position - 1,
|
position: nextStep.position - 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,10 +19,7 @@ const updateFlowResolver = async (
|
|||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
flow = await flow.$query().patchAndFetch({
|
flow = await flow.$query().patchAndFetch(params);
|
||||||
...flow,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
return flow;
|
return flow;
|
||||||
};
|
};
|
||||||
|
@@ -20,7 +20,7 @@ const getStepWithTestExecutionsResolver = async (
|
|||||||
.withGraphJoined('executionSteps')
|
.withGraphJoined('executionSteps')
|
||||||
.select('steps.*', 'executionSteps.data_out as output')
|
.select('steps.*', 'executionSteps.data_out as output')
|
||||||
.where('flow_id', '=', step.flowId)
|
.where('flow_id', '=', step.flowId)
|
||||||
.andWhere('position', '<=', step.position)
|
.andWhere('position', '<', step.position)
|
||||||
.distinctOn('executionSteps.step_id')
|
.distinctOn('executionSteps.step_id')
|
||||||
.orderBy([
|
.orderBy([
|
||||||
'executionSteps.step_id',
|
'executionSteps.step_id',
|
||||||
|
@@ -21,7 +21,9 @@ const actionType = new GraphQLObjectType({
|
|||||||
label: { type: GraphQLString },
|
label: { type: GraphQLString },
|
||||||
key: { type: GraphQLString },
|
key: { type: GraphQLString },
|
||||||
type: { type: GraphQLString },
|
type: { type: GraphQLString },
|
||||||
required: { type: GraphQLBoolean }
|
description: { type: GraphQLString },
|
||||||
|
required: { type: GraphQLBoolean },
|
||||||
|
variables: { type: GraphQLBoolean }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@@ -10,7 +10,7 @@ class Step extends Base {
|
|||||||
key: string;
|
key: string;
|
||||||
appKey: string;
|
appKey: string;
|
||||||
type!: StepEnumType;
|
type!: StepEnumType;
|
||||||
connectionId: string;
|
connectionId?: string;
|
||||||
status: string;
|
status: string;
|
||||||
position: number;
|
position: number;
|
||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
|
@@ -29,6 +29,9 @@
|
|||||||
"react-intl": "^5.20.12",
|
"react-intl": "^5.20.12",
|
||||||
"react-router-dom": "^6.0.2",
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
"slate": "^0.72.8",
|
||||||
|
"slate-history": "^0.66.0",
|
||||||
|
"slate-react": "^0.72.9",
|
||||||
"typescript": "^4.1.2",
|
"typescript": "^4.1.2",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
|
@@ -23,7 +23,7 @@ type ChooseAccountSubstepProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const optionGenerator = (connection: AppConnection): { label: string; value: string; } => ({
|
const optionGenerator = (connection: AppConnection): { label: string; value: string; } => ({
|
||||||
label: connection?.data?.screenName as string,
|
label: connection?.data?.screenName as string ?? 'Unnamed',
|
||||||
value: connection?.id as string,
|
value: connection?.id as string,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -49,7 +49,7 @@ export default function Editor(props: EditorProps): React.ReactElement {
|
|||||||
const mutationInput: Record<string, unknown> = {
|
const mutationInput: Record<string, unknown> = {
|
||||||
id: step.id,
|
id: step.id,
|
||||||
key: step.key,
|
key: step.key,
|
||||||
parameters: JSON.stringify(step.parameters, null, 2),
|
parameters: step.parameters,
|
||||||
connection: {
|
connection: {
|
||||||
id: step.connection?.id,
|
id: step.connection?.id,
|
||||||
},
|
},
|
||||||
|
@@ -68,7 +68,7 @@ export default function EditorLayout(): React.ReactElement {
|
|||||||
<Box pr={1}>
|
<Box pr={1}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch checked={flow?.active} />
|
<Switch checked={flow?.active ?? false} />
|
||||||
}
|
}
|
||||||
label={flow?.active ? formatMessage('flow.active') : formatMessage('flow.inactive')}
|
label={flow?.active ? formatMessage('flow.active') : formatMessage('flow.inactive')}
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery, useLazyQuery } from '@apollo/client';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
@@ -11,6 +11,7 @@ import IconButton from '@mui/material/IconButton';
|
|||||||
import ErrorIcon from '@mui/icons-material/Error';
|
import ErrorIcon from '@mui/icons-material/Error';
|
||||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||||
|
|
||||||
|
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||||
import TestSubstep from 'components/TestSubstep';
|
import TestSubstep from 'components/TestSubstep';
|
||||||
import FlowSubstep from 'components/FlowSubstep';
|
import FlowSubstep from 'components/FlowSubstep';
|
||||||
import ChooseAppAndEventSubstep from 'components/ChooseAppAndEventSubstep';
|
import ChooseAppAndEventSubstep from 'components/ChooseAppAndEventSubstep';
|
||||||
@@ -19,6 +20,7 @@ import Form from 'components/Form';
|
|||||||
import FlowStepContextMenu from 'components/FlowStepContextMenu';
|
import FlowStepContextMenu from 'components/FlowStepContextMenu';
|
||||||
import AppIcon from 'components/AppIcon';
|
import AppIcon from 'components/AppIcon';
|
||||||
import { GET_APPS } from 'graphql/queries/get-apps';
|
import { GET_APPS } from 'graphql/queries/get-apps';
|
||||||
|
import { GET_STEP_WITH_TEST_EXECUTIONS } from 'graphql/queries/get-step-with-test-executions';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import type { App, AppFields } from 'types/app';
|
import type { App, AppFields } from 'types/app';
|
||||||
import type { Step } from 'types/step';
|
import type { Step } from 'types/step';
|
||||||
@@ -34,35 +36,40 @@ type FlowStepProps = {
|
|||||||
onChange: (step: Step) => void;
|
onChange: (step: Step) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseStep = (step: Step) => {
|
|
||||||
try {
|
|
||||||
// stringify stringified JSON first to overcome type casting
|
|
||||||
const parameters = JSON.parse(step.parameters?.toString());
|
|
||||||
return {
|
|
||||||
...step,
|
|
||||||
parameters,
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// highly likely that step does not have any parameters and thus, the error is thrown
|
|
||||||
return {
|
|
||||||
...step,
|
|
||||||
parameters: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validIcon = <CheckCircleIcon color="success" />;
|
const validIcon = <CheckCircleIcon color="success" />;
|
||||||
const errorIcon = <ErrorIcon color="error" />;
|
const errorIcon = <ErrorIcon color="error" />;
|
||||||
|
|
||||||
export default function FlowStep(props: FlowStepProps): React.ReactElement | null {
|
export default function FlowStep(props: FlowStepProps): React.ReactElement | null {
|
||||||
const { collapsed, index, onChange } = props;
|
const { collapsed, index, onChange } = props;
|
||||||
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
const contextButtonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||||
const step: Step = React.useMemo(() => parseStep(props.step), [props.step]);
|
const step: Step = props.step;
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||||
const isTrigger = step.type === StepType.Trigger;
|
const isTrigger = step.type === StepType.Trigger;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(0);
|
const [currentSubstep, setCurrentSubstep] = React.useState<number | null>(2);
|
||||||
const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }});
|
const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }});
|
||||||
|
const [
|
||||||
|
getStepWithTestExecutions,
|
||||||
|
{
|
||||||
|
data: stepWithTestExecutionsData,
|
||||||
|
called: stepWithTestExecutionsCalled,
|
||||||
|
loading: stepWithTestExecutionsLoading,
|
||||||
|
error: stepWithTestExecutionsError
|
||||||
|
},
|
||||||
|
] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, {
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!stepWithTestExecutionsCalled && !collapsed && !isTrigger) {
|
||||||
|
getStepWithTestExecutions({
|
||||||
|
variables: {
|
||||||
|
stepId: step.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [collapsed, stepWithTestExecutionsCalled, getStepWithTestExecutions, step.id, isTrigger]);
|
||||||
|
|
||||||
const apps: App[] = data?.getApps;
|
const apps: App[] = data?.getApps;
|
||||||
const app = apps?.find((currentApp: App) => currentApp.key === step.appKey);
|
const app = apps?.find((currentApp: App) => currentApp.key === step.appKey);
|
||||||
|
|
||||||
@@ -77,6 +84,10 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
|
|||||||
setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1);
|
setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = (val: any) => {
|
||||||
|
handleChange({ step: val as Step });
|
||||||
|
}
|
||||||
|
|
||||||
if (!apps) return null;
|
if (!apps) return null;
|
||||||
|
|
||||||
const onContextMenuClose = (event: React.SyntheticEvent) => {
|
const onContextMenuClose = (event: React.SyntheticEvent) => {
|
||||||
@@ -132,6 +143,8 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
|
|||||||
<Collapse in={!collapsed} unmountOnExit>
|
<Collapse in={!collapsed} unmountOnExit>
|
||||||
<Content>
|
<Content>
|
||||||
<List>
|
<List>
|
||||||
|
<StepExecutionsProvider value={stepWithTestExecutionsData?.getStepWithTestExecutions as Step[]}>
|
||||||
|
<Form defaultValues={step} onSubmit={handleSubmit}>
|
||||||
<ChooseAppAndEventSubstep
|
<ChooseAppAndEventSubstep
|
||||||
expanded={currentSubstep === 0}
|
expanded={currentSubstep === 0}
|
||||||
substep={{ name: 'Choose app & event', arguments: [] }}
|
substep={{ name: 'Choose app & event', arguments: [] }}
|
||||||
@@ -142,7 +155,6 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
|
|||||||
step={step}
|
step={step}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form defaultValues={step.parameters}>
|
|
||||||
{substeps?.length > 0 && substeps.map((substep: { name: string, key: string, arguments: AppFields[] }, index: number) => (
|
{substeps?.length > 0 && substeps.map((substep: { name: string, key: string, arguments: AppFields[] }, index: number) => (
|
||||||
<React.Fragment key={`${substep?.name}-${index}`}>
|
<React.Fragment key={`${substep?.name}-${index}`}>
|
||||||
{substep.key === 'chooseAccount' && (
|
{substep.key === 'chooseAccount' && (
|
||||||
@@ -183,6 +195,7 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</Form>
|
</Form>
|
||||||
|
</StepExecutionsProvider>
|
||||||
</List>
|
</List>
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
@@ -26,9 +27,9 @@ const validateSubstep = (substep: Substep, step: Step) => {
|
|||||||
return args.every(arg => {
|
return args.every(arg => {
|
||||||
if (arg.required === false) { return true; }
|
if (arg.required === false) { return true; }
|
||||||
|
|
||||||
const argValue = step.parameters[arg.key];
|
const argValue = step.parameters?.[arg.key];
|
||||||
|
|
||||||
return argValue !== null && argValue !== undefined;
|
return Boolean(argValue);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,25 +49,39 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
arguments: args,
|
arguments: args,
|
||||||
} = substep;
|
} = substep;
|
||||||
|
|
||||||
const handleChangeOnBlur = React.useCallback((event: React.SyntheticEvent) => {
|
const formContext = useFormContext();
|
||||||
const { name, value: newValue } = event.target as HTMLInputElement;
|
const [validationStatus, setValidationStatus] = React.useState<boolean | null>(validateSubstep(substep, formContext.getValues() as Step));
|
||||||
const currentValue = step.parameters?.[name];
|
|
||||||
|
|
||||||
if (currentValue !== newValue) {
|
|
||||||
|
const handleChangeOnBlur = React.useCallback((key: string) => {
|
||||||
|
return (value: string) => {
|
||||||
|
const currentValue = step.parameters?.[key];
|
||||||
|
|
||||||
|
if (currentValue !== value) {
|
||||||
onChange({
|
onChange({
|
||||||
step: {
|
step: {
|
||||||
...step,
|
...step,
|
||||||
parameters: {
|
parameters: {
|
||||||
...step.parameters,
|
...step.parameters,
|
||||||
[name]: newValue,
|
[key]: value,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [step, onChange]);
|
}, [step, onChange]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function validate (step: unknown) {
|
||||||
|
const validationResult = validateSubstep(substep, step as Step);
|
||||||
|
setValidationStatus(validationResult);
|
||||||
|
};
|
||||||
|
const subscription = formContext.watch(validate);
|
||||||
|
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [substep, formContext.watch]);
|
||||||
|
|
||||||
const onToggle = expanded ? onCollapse : onExpand;
|
const onToggle = expanded ? onCollapse : onExpand;
|
||||||
const valid = validateSubstep(substep, step);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -74,22 +89,25 @@ function FlowSubstep(props: FlowSubstepProps): React.ReactElement {
|
|||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
title={name}
|
title={name}
|
||||||
valid={valid}
|
valid={validationStatus}
|
||||||
/>
|
/>
|
||||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
<ListItem sx={{ pt: 2, pb: 3, flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||||
<React.Fragment>
|
|
||||||
{args?.map((argument) => (
|
{args?.map((argument) => (
|
||||||
<InputCreator key={argument?.key} schema={argument} onBlur={handleChangeOnBlur} />
|
<InputCreator
|
||||||
|
key={argument.key}
|
||||||
|
schema={argument}
|
||||||
|
namePrefix="parameters"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
disabled={!valid}
|
disabled={!validationStatus}
|
||||||
|
type="submit"
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -16,6 +16,10 @@ export default function Form(props: FormProps): React.ReactElement {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
methods.reset(defaultValues);
|
||||||
|
}, [defaultValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
||||||
|
@@ -2,12 +2,14 @@ import * as React from 'react';
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import type { AppFields } from 'types/app';
|
import type { AppFields } from 'types/app';
|
||||||
|
|
||||||
|
import PowerInput from 'components/PowerInput';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
|
|
||||||
type InputCreatorProps = {
|
type InputCreatorProps = {
|
||||||
onChange?: React.ChangeEventHandler;
|
onChange?: React.ChangeEventHandler;
|
||||||
onBlur?: React.FocusEventHandler;
|
onBlur?: React.FocusEventHandler;
|
||||||
schema: AppFields;
|
schema: AppFields;
|
||||||
|
namePrefix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
export default function InputCreator(props: InputCreatorProps): React.ReactElement {
|
||||||
@@ -15,6 +17,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
schema,
|
schema,
|
||||||
|
namePrefix,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
@@ -27,8 +30,24 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
value,
|
value,
|
||||||
description,
|
description,
|
||||||
clickToCopy,
|
clickToCopy,
|
||||||
|
variables,
|
||||||
} = schema;
|
} = schema;
|
||||||
|
|
||||||
|
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||||
|
|
||||||
|
if (variables) {
|
||||||
|
return (
|
||||||
|
<PowerInput
|
||||||
|
label={label}
|
||||||
|
description={description}
|
||||||
|
control={control}
|
||||||
|
name={computedName}
|
||||||
|
required={required}
|
||||||
|
// onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
@@ -38,7 +57,7 @@ export default function InputCreator(props: InputCreatorProps): React.ReactEleme
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
name={name}
|
name={computedName}
|
||||||
size="small"
|
size="small"
|
||||||
label={label}
|
label={label}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
14
packages/web/src/components/Portal/index.tsx
Normal file
14
packages/web/src/components/Portal/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
type PortalProps = {
|
||||||
|
children: React.ReactElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Portal = ({ children }: PortalProps) => {
|
||||||
|
return typeof document === 'object'
|
||||||
|
? ReactDOM.createPortal(children, document.body)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Portal;
|
133
packages/web/src/components/PowerInput/Suggestions.tsx
Normal file
133
packages/web/src/components/PowerInput/Suggestions.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import MuiListItemText from '@mui/material/ListItemText';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ExpandLess from '@mui/icons-material/ExpandLess';
|
||||||
|
import ExpandMore from '@mui/icons-material/ExpandMore';
|
||||||
|
import { Step } from 'types/step';
|
||||||
|
|
||||||
|
const ListItemText = styled(MuiListItemText)``;
|
||||||
|
|
||||||
|
type SuggestionsProps = {
|
||||||
|
query?: string | null;
|
||||||
|
index: number;
|
||||||
|
data: any;
|
||||||
|
onSuggestionClick: (variable: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SHORT_LIST_LENGTH = 4;
|
||||||
|
const LIST_HEIGHT = 256;
|
||||||
|
|
||||||
|
const getPartialFilteredArray = (array: any[], query = '', length = array.length) => {
|
||||||
|
return array
|
||||||
|
.filter((suboption: any) => suboption.name.includes(query))
|
||||||
|
.slice(0, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Suggestions = (props: SuggestionsProps) => {
|
||||||
|
const {
|
||||||
|
query = '',
|
||||||
|
index: focusIndex,
|
||||||
|
data,
|
||||||
|
onSuggestionClick = () => null,
|
||||||
|
} = props;
|
||||||
|
const [current, setCurrent] = React.useState<number | null>(0);
|
||||||
|
const [listLength, setListLength] = React.useState<number | undefined>(SHORT_LIST_LENGTH);
|
||||||
|
|
||||||
|
const expandList = () => {
|
||||||
|
setListLength(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseList = () => {
|
||||||
|
setListLength(SHORT_LIST_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setListLength(SHORT_LIST_LENGTH);
|
||||||
|
}, [current])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={5}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ p: 2, }}>Variables</Typography>
|
||||||
|
<List
|
||||||
|
disablePadding
|
||||||
|
>
|
||||||
|
{data.map((option: Step, index: number) => (
|
||||||
|
<>
|
||||||
|
<ListItemButton
|
||||||
|
divider
|
||||||
|
onClick={() => setCurrent((currentIndex) => currentIndex === index ? null : index)}
|
||||||
|
sx={{ py: 0.5, }}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={option.name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{option.output?.length && (
|
||||||
|
current === index ? <ExpandLess /> : <ExpandMore />
|
||||||
|
)}
|
||||||
|
</ListItemButton>
|
||||||
|
|
||||||
|
<Collapse in={current === index} timeout="auto" unmountOnExit>
|
||||||
|
<List component="div" disablePadding sx={{ maxHeight: LIST_HEIGHT, overflowY: 'auto' }}>
|
||||||
|
{getPartialFilteredArray(option.output as any || [], query as string, listLength)
|
||||||
|
.map((suboption: any, index: number) => (
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ pl: 4 }}
|
||||||
|
divider
|
||||||
|
onClick={() => onSuggestionClick(suboption)}
|
||||||
|
selected={focusIndex === index}>
|
||||||
|
<ListItemText
|
||||||
|
primary={suboption.name}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
variant: 'subtitle1',
|
||||||
|
title: 'Property name',
|
||||||
|
sx: { fontWeight: 700 }
|
||||||
|
}}
|
||||||
|
secondary={suboption.value || ''}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
variant: 'subtitle2',
|
||||||
|
title: 'Sample value',
|
||||||
|
noWrap: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{listLength && (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={expandList}
|
||||||
|
>
|
||||||
|
Show all
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{listLength === undefined && (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={collapseList}
|
||||||
|
>
|
||||||
|
Show less
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Collapse>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Suggestions;
|
44
packages/web/src/components/PowerInput/data.ts
Normal file
44
packages/web/src/components/PowerInput/data.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Step } from 'types/step';
|
||||||
|
|
||||||
|
const joinBy = (delimiter = '.', ...args: string[]) => args.filter(Boolean).join(delimiter);
|
||||||
|
|
||||||
|
const process = (data: any, parentKey?: any, index?: number): any[] => {
|
||||||
|
if (typeof data !== 'object') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: `${parentKey}.${index}`,
|
||||||
|
value: data,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
|
||||||
|
return entries.flatMap(([name, value]) => {
|
||||||
|
const fullName = joinBy('.', parentKey, (index as number)?.toString(), name);
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((item, index) => process(item, fullName, index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
return process(value, fullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
name: fullName,
|
||||||
|
value,
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processStepWithExecutions = (steps: Step[]): any[] => {
|
||||||
|
if (!steps) return [];
|
||||||
|
|
||||||
|
return steps.map((step: Step, index: number) => ({
|
||||||
|
id: step.id,
|
||||||
|
// TODO: replace with step.name once introduced
|
||||||
|
name: `${index + 1}. ${step.appKey}`,
|
||||||
|
output: process(step.output, `step.${step.id}`),
|
||||||
|
}));
|
||||||
|
};
|
247
packages/web/src/components/PowerInput/index.tsx
Normal file
247
packages/web/src/components/PowerInput/index.tsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
|
import { Controller, Control, FieldValues } from 'react-hook-form';
|
||||||
|
import { Editor, Transforms, Range, createEditor } from 'slate';
|
||||||
|
import {
|
||||||
|
Slate,
|
||||||
|
Editable,
|
||||||
|
useSelected,
|
||||||
|
useFocused,
|
||||||
|
} from 'slate-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
serialize,
|
||||||
|
deserialize,
|
||||||
|
insertVariable,
|
||||||
|
customizeEditor,
|
||||||
|
} from './utils';
|
||||||
|
import Suggestions from './Suggestions';
|
||||||
|
import { StepExecutionsContext } from 'contexts/StepExecutions';
|
||||||
|
|
||||||
|
import { FakeInput, InputLabelWrapper } from './style';
|
||||||
|
import { VariableElement } from './types';
|
||||||
|
import { processStepWithExecutions } from './data';
|
||||||
|
|
||||||
|
type PowerInputProps = {
|
||||||
|
control?: Control<FieldValues>;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
onBlur?: (value: string) => void;
|
||||||
|
defaultValue?: string;
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
description?: string;
|
||||||
|
docUrl?: string;
|
||||||
|
clickToCopy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PowerInput = (props: PowerInputProps) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
defaultValue = '',
|
||||||
|
onBlur,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
required,
|
||||||
|
description,
|
||||||
|
} = props;
|
||||||
|
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
|
||||||
|
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
const [target, setTarget] = React.useState<Range | null>(null);
|
||||||
|
const [index, setIndex] = React.useState(0);
|
||||||
|
const [search, setSearch] = React.useState<string | null>(null);
|
||||||
|
const renderElement = React.useCallback(props => <Element {...props} />, []);
|
||||||
|
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||||
|
|
||||||
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
|
}, [priorStepsWithExecutions])
|
||||||
|
|
||||||
|
const handleBlur = React.useCallback((value) => {
|
||||||
|
onBlur?.(value);
|
||||||
|
}, [onBlur]);
|
||||||
|
|
||||||
|
const handleVariableSuggestionClick = React.useCallback(
|
||||||
|
(variable: Pick<VariableElement, "name" | "value">) => {
|
||||||
|
if (target) {
|
||||||
|
Transforms.select(editor, target);
|
||||||
|
insertVariable(editor, variable);
|
||||||
|
setTarget(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[index, target]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onKeyDown = React.useCallback(
|
||||||
|
event => {
|
||||||
|
if (target) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown': {
|
||||||
|
event.preventDefault();
|
||||||
|
setIndex((currentIndex) => currentIndex + 1);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
event.preventDefault();
|
||||||
|
setIndex((currentIndex) => currentIndex - 1 < 0 ? 0 : currentIndex - 1);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Tab':
|
||||||
|
case 'Enter': {
|
||||||
|
event.preventDefault();
|
||||||
|
Transforms.select(editor, target);
|
||||||
|
insertVariable(editor, stepsWithVariables[0].output[index]);
|
||||||
|
setTarget(null);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'Escape': {
|
||||||
|
event.preventDefault();
|
||||||
|
setTarget(null);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[index, search, target, stepsWithVariables]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
rules={{ required }}
|
||||||
|
name={name}
|
||||||
|
control={control}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
shouldUnregister={false}
|
||||||
|
render={({ field: { value, ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field } }) => (
|
||||||
|
<Slate
|
||||||
|
editor={editor}
|
||||||
|
value={deserialize(value)}
|
||||||
|
onChange={value => {
|
||||||
|
controllerOnChange(serialize(value));
|
||||||
|
const { selection } = editor
|
||||||
|
|
||||||
|
if (selection && Range.isCollapsed(selection)) {
|
||||||
|
const [start] = Range.edges(selection);
|
||||||
|
const lineBefore = Editor.before(editor, start, { unit: 'line' });
|
||||||
|
const before = lineBefore && Editor.before(editor, lineBefore);
|
||||||
|
const beforeRange = (before || lineBefore) && Editor.range(editor, before || lineBefore, start);
|
||||||
|
const beforeText = beforeRange && Editor.string(editor, beforeRange);
|
||||||
|
const variableMatch = beforeText && beforeText.match(/@([\w.]*?)$/);
|
||||||
|
|
||||||
|
if (variableMatch) {
|
||||||
|
const beginningOfVariable = Editor.before(
|
||||||
|
editor,
|
||||||
|
start,
|
||||||
|
{
|
||||||
|
unit: 'offset',
|
||||||
|
distance: (variableMatch[1].length || 0) + 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (beginningOfVariable) {
|
||||||
|
const newTarget = Editor.range(editor, beginningOfVariable, start);
|
||||||
|
if (newTarget) {
|
||||||
|
setTarget(newTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIndex(0);
|
||||||
|
setSearch(variableMatch[1]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearch(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClickAwayListener onClickAway={() => setSearch(null)}>
|
||||||
|
{/* ref-able single child for ClickAwayListener */}
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<FakeInput>
|
||||||
|
<InputLabelWrapper>
|
||||||
|
<InputLabel
|
||||||
|
shrink={true}
|
||||||
|
// focused
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ bgcolor: 'white', display: 'inline-block', px: .75 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</InputLabel>
|
||||||
|
</InputLabelWrapper>
|
||||||
|
|
||||||
|
<Editable
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
renderElement={renderElement}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={() => { controllerOnBlur(); handleBlur(value); }}
|
||||||
|
/>
|
||||||
|
</FakeInput>
|
||||||
|
{/* ghost placer for the variables popover */}
|
||||||
|
<div ref={editorRef} style={{ width: '100%' }} />
|
||||||
|
|
||||||
|
<FormHelperText
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</FormHelperText>
|
||||||
|
|
||||||
|
<Popper
|
||||||
|
open={target !== null && search !== null}
|
||||||
|
anchorEl={editorRef.current}
|
||||||
|
style={{ width: editorRef.current?.clientWidth }}
|
||||||
|
>
|
||||||
|
<Suggestions
|
||||||
|
query={search}
|
||||||
|
index={index}
|
||||||
|
data={stepsWithVariables}
|
||||||
|
onSuggestionClick={handleVariableSuggestionClick}
|
||||||
|
/>
|
||||||
|
</Popper>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Slate>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Element = (props: any) => {
|
||||||
|
const { attributes, children, element } = props;
|
||||||
|
switch (element.type) {
|
||||||
|
case 'variable':
|
||||||
|
return <Variable {...props} />;
|
||||||
|
default:
|
||||||
|
return <p {...attributes}>{children}</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Variable = ({ attributes, children, element }: any) => {
|
||||||
|
const selected = useSelected();
|
||||||
|
const focused = useFocused();
|
||||||
|
const label = (
|
||||||
|
<>
|
||||||
|
{element.name}
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
{...attributes}
|
||||||
|
component="span"
|
||||||
|
contentEditable={false}
|
||||||
|
style={{
|
||||||
|
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PowerInput;
|
28
packages/web/src/components/PowerInput/style.ts
Normal file
28
packages/web/src/components/PowerInput/style.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
|
||||||
|
export const InputLabelWrapper = styled('div')`
|
||||||
|
position: absolute;
|
||||||
|
left: ${({ theme }) => theme.spacing(1.75)};
|
||||||
|
inset: 0;
|
||||||
|
left: -6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FakeInput = styled('div')`
|
||||||
|
border: 1px solid #eee;
|
||||||
|
min-height: 52px;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: ${({ theme }) => theme.spacing(0, 1.75)};
|
||||||
|
border-radius: ${({ theme }) => theme.spacing(.5)};
|
||||||
|
border-color: rgba(0, 0, 0, 0.23);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: ${({ theme }) => theme.palette.text.primary};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: ${({ theme }) => theme.palette.primary.main};
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
`;
|
25
packages/web/src/components/PowerInput/types.ts
Normal file
25
packages/web/src/components/PowerInput/types.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { BaseEditor, Text, Descendant } from 'slate';
|
||||||
|
import type { ReactEditor } from 'slate-react';
|
||||||
|
|
||||||
|
export type VariableElement = {
|
||||||
|
type: 'variable';
|
||||||
|
value?: unknown;
|
||||||
|
name?: string;
|
||||||
|
children: Text[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParagraphElement = {
|
||||||
|
type: 'paragraph';
|
||||||
|
children: Descendant[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomEditor = BaseEditor & ReactEditor;
|
||||||
|
|
||||||
|
export type CustomElement = VariableElement | ParagraphElement;
|
||||||
|
|
||||||
|
declare module 'slate' {
|
||||||
|
interface CustomTypes {
|
||||||
|
Editor: CustomEditor;
|
||||||
|
Element: CustomElement;
|
||||||
|
}
|
||||||
|
}
|
91
packages/web/src/components/PowerInput/utils.ts
Normal file
91
packages/web/src/components/PowerInput/utils.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Text, Descendant, Transforms } from 'slate';
|
||||||
|
import { withHistory } from 'slate-history';
|
||||||
|
import { withReact } from 'slate-react';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CustomEditor,
|
||||||
|
CustomElement,
|
||||||
|
VariableElement,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const deserialize = (value: string): Descendant[] => {
|
||||||
|
const variableRegExp = /({{.*?}})/g;
|
||||||
|
|
||||||
|
if (!value) return [{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: '', }],
|
||||||
|
}];
|
||||||
|
|
||||||
|
return value.split('\n').map(line => {
|
||||||
|
const nodes = line.split(variableRegExp);
|
||||||
|
|
||||||
|
if (nodes.length > 1) {
|
||||||
|
return {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: nodes.map(node => {
|
||||||
|
if (node.match(variableRegExp)) {
|
||||||
|
return {
|
||||||
|
type: 'variable',
|
||||||
|
name: node.replace(/{{|}}/g, ''),
|
||||||
|
children: [{ text: '' }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: node,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [{ text: line }],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serialize = (value: Descendant[]): string => {
|
||||||
|
return value.map(node => serializeNode(node)).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeNode = (node: CustomElement | Descendant): string => {
|
||||||
|
if (Text.isText(node)) {
|
||||||
|
return node.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'variable') {
|
||||||
|
return `{{${node.name}}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.children.map(n => serializeNode(n)).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withVariables = (editor: CustomEditor) => {
|
||||||
|
const { isInline, isVoid } = editor;
|
||||||
|
|
||||||
|
editor.isInline = (element: CustomElement) => {
|
||||||
|
return element.type === 'variable' ? true : isInline(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.isVoid = (element: CustomElement) => {
|
||||||
|
return element.type === 'variable' ? true : isVoid(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertVariable = (editor: CustomEditor, variableData: Pick<VariableElement, "name" | "value">) => {
|
||||||
|
const variable: VariableElement = {
|
||||||
|
type: 'variable',
|
||||||
|
name: variableData.name,
|
||||||
|
value: variableData.value,
|
||||||
|
children: [{ text: '' }],
|
||||||
|
};
|
||||||
|
Transforms.insertNodes(editor, variable);
|
||||||
|
Transforms.move(editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customizeEditor = (editor: CustomEditor): CustomEditor => {
|
||||||
|
return withVariables(withReact(withHistory(editor)));
|
||||||
|
};
|
20
packages/web/src/contexts/StepExecutions.tsx
Normal file
20
packages/web/src/contexts/StepExecutions.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Step } from 'types/step';
|
||||||
|
|
||||||
|
export const StepExecutionsContext = React.createContext<Step[]>([]);
|
||||||
|
|
||||||
|
type StepExecutionsProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: Step[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepExecutionsProvider = (props: StepExecutionsProviderProps): React.ReactElement => {
|
||||||
|
const { children, value } = props;
|
||||||
|
return (
|
||||||
|
<StepExecutionsContext.Provider
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StepExecutionsContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@@ -76,6 +76,8 @@ export const GET_APPS = gql`
|
|||||||
key
|
key
|
||||||
type
|
type
|
||||||
required
|
required
|
||||||
|
description
|
||||||
|
variables
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const GET_STEP_WITH_TEST_EXECUTIONS = gql`
|
||||||
|
query GetStepWithTestExecutions($stepId: String!) {
|
||||||
|
getStepWithTestExecutions(stepId: $stepId) {
|
||||||
|
id
|
||||||
|
appKey
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@@ -40,10 +40,10 @@ function getObjectOfEntries(iterator: any) {
|
|||||||
|
|
||||||
const processOpenWithPopup = (step: Step, variables: Record<string, string>) => {
|
const processOpenWithPopup = (step: Step, variables: Record<string, string>) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const windowFeatures = 'toolbar=no, menubar=no, width=600, height=700, top=100, left=100';
|
const windowFeatures = 'toolbar=no, titlebar=no, menubar=no, width=500, height=700, top=100, left=100';
|
||||||
const url = variables.url;
|
const url = variables.url;
|
||||||
|
|
||||||
const popup: any = window.open(url, '_blank', windowFeatures);
|
const popup = window.open(url, '_blank', windowFeatures) as WindowProxy;
|
||||||
popup?.focus();
|
popup?.focus();
|
||||||
|
|
||||||
const messageHandler = async (event: any) => {
|
const messageHandler = async (event: any) => {
|
||||||
|
@@ -3,12 +3,13 @@ type AppFields = {
|
|||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: string;
|
||||||
required: boolean,
|
required: boolean;
|
||||||
readOnly: boolean,
|
readOnly: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
description: string;
|
description: string;
|
||||||
docUrl: string;
|
docUrl: string;
|
||||||
clickToCopy: boolean,
|
clickToCopy: boolean;
|
||||||
|
variables?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppConnection = {
|
type AppConnection = {
|
||||||
|
@@ -16,6 +16,7 @@ export type Step = {
|
|||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
connection: Pick<Connection, 'id' | 'verified'>;
|
connection: Pick<Connection, 'id' | 'verified'>;
|
||||||
status: 'completed' | 'incomplete';
|
status: 'completed' | 'incomplete';
|
||||||
|
output: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Substep = {
|
export type Substep = {
|
||||||
|
66
yarn.lock
66
yarn.lock
@@ -4116,6 +4116,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/is-hotkey@^0.1.1":
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/is-hotkey/-/is-hotkey-0.1.7.tgz#30ec6d4234895230b576728ef77e70a52962f3b3"
|
||||||
|
integrity sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
|
||||||
@@ -4168,7 +4173,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/lodash" "*"
|
"@types/lodash" "*"
|
||||||
|
|
||||||
"@types/lodash@*":
|
"@types/lodash@*", "@types/lodash@^4.14.149":
|
||||||
version "4.14.178"
|
version "4.14.178"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
|
||||||
integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
|
integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
|
||||||
@@ -6510,6 +6515,11 @@ compression@^1.7.4:
|
|||||||
safe-buffer "5.1.2"
|
safe-buffer "5.1.2"
|
||||||
vary "~1.1.2"
|
vary "~1.1.2"
|
||||||
|
|
||||||
|
compute-scroll-into-view@^1.0.17:
|
||||||
|
version "1.0.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
|
||||||
|
integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
@@ -7437,6 +7447,11 @@ dir-glob@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
path-type "^4.0.0"
|
||||||
|
|
||||||
|
direction@^1.0.3:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/direction/-/direction-1.0.4.tgz#2b86fb686967e987088caf8b89059370d4837442"
|
||||||
|
integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==
|
||||||
|
|
||||||
discord-api-types@^0.22.0:
|
discord-api-types@^0.22.0:
|
||||||
version "0.22.0"
|
version "0.22.0"
|
||||||
resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a"
|
resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.22.0.tgz#34dc57fe8e016e5eaac5e393646cd42a7e1ccc2a"
|
||||||
@@ -10292,6 +10307,11 @@ is-hexadecimal@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
|
||||||
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
|
||||||
|
|
||||||
|
is-hotkey@^0.1.6:
|
||||||
|
version "0.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.8.tgz#6b1f4b2d0e5639934e20c05ed24d623a21d36d25"
|
||||||
|
integrity sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==
|
||||||
|
|
||||||
is-installed-globally@^0.4.0:
|
is-installed-globally@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
|
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
|
||||||
@@ -11723,7 +11743,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||||
|
|
||||||
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
@@ -15641,6 +15661,13 @@ scoped-regex@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f"
|
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f"
|
||||||
integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ==
|
integrity sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ==
|
||||||
|
|
||||||
|
scroll-into-view-if-needed@^2.2.20:
|
||||||
|
version "2.2.29"
|
||||||
|
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885"
|
||||||
|
integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg==
|
||||||
|
dependencies:
|
||||||
|
compute-scroll-into-view "^1.0.17"
|
||||||
|
|
||||||
section-matter@^1.0.0:
|
section-matter@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
|
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
|
||||||
@@ -15941,6 +15968,36 @@ slash@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||||
|
|
||||||
|
slate-history@^0.66.0:
|
||||||
|
version "0.66.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940"
|
||||||
|
integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==
|
||||||
|
dependencies:
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
|
||||||
|
slate-react@^0.72.9:
|
||||||
|
version "0.72.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.72.9.tgz#b05dd533bd29dd2d4796b614a8d8e01f214bb714"
|
||||||
|
integrity sha512-FEsqB+D1R/h+w1eCtHH367Krw2X7vju2GjMRL/d0bUiCRXlV50J9I9TJizvi7aaZyqBY8BypCuIiq9nNmsulCA==
|
||||||
|
dependencies:
|
||||||
|
"@types/is-hotkey" "^0.1.1"
|
||||||
|
"@types/lodash" "^4.14.149"
|
||||||
|
direction "^1.0.3"
|
||||||
|
is-hotkey "^0.1.6"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
lodash "^4.17.4"
|
||||||
|
scroll-into-view-if-needed "^2.2.20"
|
||||||
|
tiny-invariant "1.0.6"
|
||||||
|
|
||||||
|
slate@^0.72.8:
|
||||||
|
version "0.72.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81"
|
||||||
|
integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==
|
||||||
|
dependencies:
|
||||||
|
immer "^9.0.6"
|
||||||
|
is-plain-object "^5.0.0"
|
||||||
|
tiny-warning "^1.0.3"
|
||||||
|
|
||||||
slice-ansi@^3.0.0:
|
slice-ansi@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
|
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
|
||||||
@@ -16858,6 +16915,11 @@ timsort@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
|
tiny-invariant@1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
|
||||||
|
integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
|
||||||
|
Reference in New Issue
Block a user