diff --git a/src/cloud/components/Automations/EventInfo.tsx b/src/cloud/components/Automations/EventInfo.tsx index f3e07b443e..cace3f41d2 100644 --- a/src/cloud/components/Automations/EventInfo.tsx +++ b/src/cloud/components/Automations/EventInfo.tsx @@ -1,9 +1,9 @@ import React from 'react' -import { JsonTypeDef } from '../../lib/automations/events' +import { BoostType } from '../../lib/automations' interface EventSelectProps { name: string - typeDef: JsonTypeDef + typeDef: BoostType } const EventInfo = ({ typeDef }: EventSelectProps) => { diff --git a/src/cloud/components/Automations/FilterBuilder.tsx b/src/cloud/components/Automations/FilterBuilder.tsx index 02bf9a57ff..85ec3193d4 100644 --- a/src/cloud/components/Automations/FilterBuilder.tsx +++ b/src/cloud/components/Automations/FilterBuilder.tsx @@ -10,11 +10,12 @@ import FormSelect, { import FormRow from '../../../design/components/molecules/Form/templates/FormRow' import FormRowItem from '../../../design/components/molecules/Form/templates/FormRowItem' import { SerializedPipe } from '../../interfaces/db/automations' -import { JsonTypeDef } from '../../lib/automations/events' +import { BoostType } from '../../lib/automations' +import { flattenType } from '../../lib/automations/types' import { flattenObj } from '../../lib/utils/object' interface FilterBuilderProps { - typeDef: JsonTypeDef + typeDef: BoostType filter: SerializedPipe['filter'] onChange: (filter: SerializedPipe['filter']) => void } @@ -27,15 +28,20 @@ const FilterBuilder = ({ typeDef, filter, onChange }: FilterBuilderProps) => { string | number | boolean | undefined >() - const flattenedTypeKeys = useMemo( - () => - Object.entries(flattenObj(typeDef as any)).map(([key, val]) => ({ + const flattenedTypeKeys = useMemo(() => { + const supportedPrimitives = new Set(['number', 'string', 'boolean']) + return Array.from(flattenType(typeDef)) + .filter( + ([, type]) => + type.type === 'primitive' && supportedPrimitives.has(type.def) + ) + .map(([path, type]) => [path.join('.'), type] as const) + .map(([key, val]) => ({ label: key, value: key, - type: val, - })), - [typeDef] - ) + type: val.def, + })) + }, [typeDef]) const flattenedFilter = useMemo(() => flattenObj(filter), [filter]) diff --git a/src/cloud/components/Automations/PipeBuilder.tsx b/src/cloud/components/Automations/PipeBuilder.tsx index 6d6801ef1e..8a3ebf7bcc 100644 --- a/src/cloud/components/Automations/PipeBuilder.tsx +++ b/src/cloud/components/Automations/PipeBuilder.tsx @@ -1,5 +1,5 @@ import { mdiPlus } from '@mdi/js' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import Button from '../../../design/components/atoms/Button' import Form from '../../../design/components/molecules/Form' import FormInput from '../../../design/components/molecules/Form/atoms/FormInput' @@ -8,10 +8,10 @@ import FormRow from '../../../design/components/molecules/Form/templates/FormRow import FormRowItem from '../../../design/components/molecules/Form/templates/FormRowItem' import styled from '../../../design/lib/styled' import { SerializedPipe } from '../../interfaces/db/automations' +import { OpNode, StructNode } from '../../lib/automations/ast' import supportedEvents from '../../lib/automations/events' import CreateDocActionConfigurator from './actions/CreateDocActionConfigurator' import UpdateDocActionConfigurator from './actions/UpdateDocActionConfigurator' -import EventInfo from './EventInfo' import FilterBuilder from './FilterBuilder' const SUPPORTED_EVENT_NAMES = Object.keys(supportedEvents).map((key) => { @@ -19,8 +19,8 @@ const SUPPORTED_EVENT_NAMES = Object.keys(supportedEvents).map((key) => { }) const SUPPORTED_ACTION_OPTIONS = [ - { value: 'boost.doc.create', label: 'boost.doc.create' }, - { value: 'boost.doc.update', label: 'boost.doc.update' }, + { value: 'boost.docs.create', label: 'boost.docs.create' }, + { value: 'boost.docs.update', label: 'boost.docs.update' }, ] interface PipeBuilderProps { @@ -34,11 +34,23 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { }, [pipe.event]) const action = useMemo(() => { + if (pipe.configuration.type !== 'operation') { + return SUPPORTED_ACTION_OPTIONS[0] + } + + const identifier = pipe.configuration.identifier return ( - SUPPORTED_ACTION_OPTIONS.find(({ value }) => value === pipe.action) || + SUPPORTED_ACTION_OPTIONS.find(({ value }) => value === identifier) || SUPPORTED_ACTION_OPTIONS[0] ) - }, [pipe.action]) + }, [pipe.configuration]) + + const updateConfig = useCallback( + (input: SerializedPipe['configuration']['input']) => { + onChange({ ...pipe, configuration: { ...pipe.configuration, input } }) + }, + [pipe, onChange] + ) return ( @@ -63,11 +75,7 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { - {currentEvent != null ? ( - - ) : ( -
Select Event
- )} +
Select Event
@@ -96,22 +104,27 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { onChange({ ...pipe, action: value })} + onChange={({ value }) => + onChange({ + ...pipe, + configuration: OpNode(value, StructNode({})), + }) + } /> - {action.value === 'boost.doc.create' && ( + {action.value === 'boost.docs.create' && ( onChange({ ...pipe, configuration })} + configuration={pipe.configuration.input} + onChange={updateConfig} eventType={currentEvent} /> )} - {action.value === 'boost.doc.update' && ( + {action.value === 'boost.docs.update' && ( onChange({ ...pipe, configuration })} + configuration={pipe.configuration.input} + onChange={updateConfig} eventType={currentEvent} /> )} diff --git a/src/cloud/components/Automations/WorkflowBuilder.tsx b/src/cloud/components/Automations/WorkflowBuilder.tsx index f21fa3404a..fe39b78e0a 100644 --- a/src/cloud/components/Automations/WorkflowBuilder.tsx +++ b/src/cloud/components/Automations/WorkflowBuilder.tsx @@ -126,17 +126,17 @@ const Container = styled.div` } ` -const defaultPipe = { +const defaultPipe: SerializedPipe = { name: 'New Pipeline', event: 'github.issues.opened', - action: 'boost.doc.create', configuration: { - title: '$event.issue.title', - content: '$event.issue.body', - props: { - IssueID: { - type: 'number', - data: '$event.issue.id', + type: 'operation', + identifier: 'boost.docs.create', + input: { + type: 'constructor', + info: { + type: 'struct', + refs: {}, }, }, }, diff --git a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx index 5e24023586..944232261e 100644 --- a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx +++ b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx @@ -1,7 +1,10 @@ -import React, { useMemo, useState } from 'react' +import React, { useMemo } from 'react' import FormSelect from '../../../../design/components/molecules/Form/atoms/FormSelect' import FormRowItem from '../../../../design/components/molecules/Form/templates/FormRowItem' import { pickBy } from 'ramda' +import { BoostAST, BoostPrimitives, BoostType } from '../../../lib/automations' +import { LiteralNode, RefNode } from '../../../lib/automations/ast' +import { StdPrimitives } from '../../../lib/automations/types' const CONFIG_TYPES = [ { label: 'Event', value: 'event' }, @@ -9,14 +12,15 @@ const CONFIG_TYPES = [ ] interface ActionConfigurationInputProps { - onChange: (value: any) => void - value: any + onChange: (value: BoostAST | undefined) => void + value: BoostAST customInput: ( onChange: ActionConfigurationInputProps['onChange'], - value: any + value: Extract | null ) => React.ReactNode - eventDataOptions: Record - type?: string + eventDataOptions: Record + type: BoostPrimitives | StdPrimitives + defaultValue: any } const ActionConfigurationInput = ({ value, @@ -24,55 +28,81 @@ const ActionConfigurationInput = ({ onChange, customInput, type: dataType, + defaultValue, }: ActionConfigurationInputProps) => { - const [type, setType] = useState(() => { - if (typeof value === 'string') { - if (value.startsWith('$event')) { + const type = useMemo(() => { + if (value == null) { + return CONFIG_TYPES[1] + } + + if (value.type === 'reference') { + if (value.identifier.startsWith('$event')) { return CONFIG_TYPES[0] } - if (value.startsWith('$env')) { + if (value.identifier.startsWith('$env')) { return CONFIG_TYPES[1] } } return CONFIG_TYPES[1] - }) + }, [value]) const options = useMemo(() => { return Object.keys( - pickBy((val) => dataType == null || val === dataType, eventDataOptions) + pickBy( + (val) => val.type === 'primitive' && val.def === dataType, + eventDataOptions + ) ).map((key) => ({ label: key, value: key })) }, [eventDataOptions, dataType]) const normalized = useMemo(() => { - if (typeof value === 'string') { - if (value.startsWith('$event')) { - return value.substr('$event.'.length) + if (value == null) { + return '' + } + + if (value.type === 'reference') { + if (value.identifier.startsWith('$event')) { + return value.identifier.substr('$event.'.length) } - if (value.startsWith('$env')) { - return value.substr('$env.'.length) + if (value.identifier.startsWith('$env')) { + return value.identifier.substr('$env.'.length) } } - return typeof value === 'string' || typeof value === 'number' - ? value.toString() - : '' + return value.type === 'literal' ? value.value?.toString() || '' : '' }, [value]) return ( <> - + { + if (val.value === 'event') { + onChange(RefNode('$event.')) + } else { + onChange(LiteralNode(dataType, defaultValue)) + } + }} + /> {type.value === 'event' && ( onChange(`$event.${value}`)} + onChange={({ value }) => onChange(RefNode(`$event.${value}`))} /> )} - {type.value === 'custom' && customInput(onChange, value)} + {type.value === 'custom' && + customInput( + onChange, + value == null || value.type !== 'literal' + ? LiteralNode(dataType, defaultValue) + : value + )} ) diff --git a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx index b81d3b6b87..c6652f2a52 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -1,14 +1,21 @@ import { mdiFileDocumentOutline } from '@mdi/js' +import { dissoc } from 'ramda' import React, { useMemo } from 'react' import FormEmoji from '../../../../design/components/molecules/Form/atoms/FormEmoji' import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' import FormTextarea from '../../../../design/components/molecules/Form/atoms/FormTextArea' import FormRow from '../../../../design/components/molecules/Form/templates/FormRow' -import { flattenObj } from '../../../lib/utils/object' +import { BoostAST } from '../../../lib/automations' +import { + LiteralNode, + RecordNode, + StructNode, +} from '../../../lib/automations/ast' +import { flattenType } from '../../../lib/automations/types' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' import FolderSelect from './FolderSelect' -import PropertySelect from './PropertySelect' +import PropertySelect, { SupportedType } from './PropertySelect' const CreateDocActionConfigurator = ({ configuration, @@ -16,22 +23,48 @@ const CreateDocActionConfigurator = ({ eventType, }: ActionConfiguratorProps) => { const eventDataOptions = useMemo(() => { - return flattenObj(eventType as any) + return Object.fromEntries( + Array.from(flattenType(eventType)).map(([path, type]) => [ + path.join('.'), + type, + ]) + ) }, [eventType]) + const constructorTree = useMemo(() => { + if ( + configuration.type !== 'constructor' || + configuration.info.type !== 'struct' + ) { + return {} + } + return configuration.info.refs + }, [configuration]) + return (
onChange({ ...configuration, title })} + defaultValue='' + onChange={(title) => + onChange( + StructNode( + title != null + ? { ...constructorTree, title } + : dissoc('title', title) + ) + ) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + value={value?.value || ''} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -39,16 +72,31 @@ const CreateDocActionConfigurator = ({ onChange({ ...configuration, emoji })} + value={constructorTree.emoji} + type='string' + defaultValue='' + onChange={(emoji) => + onChange( + StructNode( + emoji != null + ? { ...constructorTree, emoji } + : dissoc('emoji', constructorTree) + ) + ) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( + onChange( + emojiStr != null + ? LiteralNode('string', emojiStr) + : undefined + ) + } /> ) }} @@ -56,15 +104,26 @@ const CreateDocActionConfigurator = ({ onChange({ ...configuration, content })} + value={constructorTree.content} + type='string' + defaultValue='' + onChange={(content) => + onChange( + StructNode( + content != null + ? { ...constructorTree, content } + : dissoc('content', constructorTree) + ) + ) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + value={value?.value} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -72,25 +131,57 @@ const CreateDocActionConfigurator = ({ - onChange({ ...configuration, parentFolder }) + onChange( + StructNode( + parentFolder != null + ? { ...constructorTree, parentFolder } + : dissoc('parentFolder', constructorTree) + ) + ) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { - return + return ( + + onChange(id != null ? LiteralNode('folder', id) : undefined) + } + /> + ) }} /> onChange({ ...configuration, props })} + value={ + constructorTree.props != null && + constructorTree.props.type === 'constructor' && + constructorTree.props.info.type === 'record' + ? constructorTree.props.info.refs.filter(isSupportedType) + : [] + } + onChange={(props) => + onChange(StructNode({ ...constructorTree, props: RecordNode(props) })) + } eventDataOptions={eventDataOptions} />
) } +function isSupportedType(x: { + key: BoostAST + val: BoostAST +}): x is SupportedType { + return ( + x.key.type === 'literal' && + (x.val.type === 'operation' || x.val.type === 'literal') + ) +} + export default CreateDocActionConfigurator diff --git a/src/cloud/components/Automations/actions/PropertySelect.tsx b/src/cloud/components/Automations/actions/PropertySelect.tsx index 7bbff07255..1e747f4f87 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -1,6 +1,5 @@ -import { dissoc } from 'ramda' import { mdiClose, mdiPlus } from '@mdi/js' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import Button from '../../../../design/components/atoms/Button' import FormRow from '../../../../design/components/molecules/Form/templates/FormRow' import FormRowItem from '../../../../design/components/molecules/Form/templates/FormRowItem' @@ -10,18 +9,21 @@ import PropRegisterCreationForm from '../../Props/PropRegisterModal/PropRegister import ActionConfigurationInput from './ActionConfigurationInput' import { useModal } from '../../../../design/lib/stores/modal' import { PropData } from '../../../interfaces/db/props' -import { SerializedPropData } from '../../../interfaces/db/props' - -type PlaceholderPropData = Omit & { - data: PropData['data'] | string -} +import { BoostAST, BoostPrimitives } from '../../../lib/automations' +import { LiteralNode, OpNode, StructNode } from '../../../lib/automations/ast' +import { StdPrimitives } from '../../../lib/automations/types' export interface PropertySelectProps { - value: Record - onChange: (props: Record) => void + value: SupportedType[] + onChange: (props: SupportedType[]) => void eventDataOptions: Record } +export type SupportedType = { + key: Extract + val: Extract +} + const PropertySelect = ({ value, onChange, @@ -30,32 +32,74 @@ const PropertySelect = ({ const { openContextModal } = useModal() const props = useMemo(() => { - return Object.entries(value) as [string, SerializedPropData][] + return value.map((ref) => { + return { key: ref.key.value, val: ref.val } + }) }, [value]) + const addProp = useCallback( + (key: string, val: SupportedType['val']) => { + onChange( + value + .filter((ref) => key !== ref.key.value) + .concat([{ key: LiteralNode('string', key), val }]) + ) + }, + [value, onChange] + ) + return ( <> - {props.map(([propName, propData]) => { - const iconPath = getIconPathOfPropType( - propData.subType || propData.type - ) + {props.map(({ key, val }, i) => { + const [propType, subType] = getPropTypeFromAst(val) + const iconPath = getIconPathOfPropType(subType || propType) return ( - + - onChange({ - ...value, - [propName]: { ...propData, data }, - }) + value={ + val.type === 'operation' && + val.input.type === 'constructor' && + val.input.info.type === 'struct' + ? val.input.info.refs.data || + LiteralNode('propData', { type: 'string', data: null }) + : val } + type={getDataTypeForPropType(propType)} + defaultValue={{ + type: propType, + subType, + data: null, + }} + onChange={(data) => { + if (data.type === 'literal') { + addProp(key, data) + } else { + addProp( + key, + OpNode( + 'boost.props.make', + StructNode( + subType != null + ? { + type: LiteralNode('string', propType), + subType: LiteralNode('string', subType), + data, + } + : { + type: LiteralNode('string', propType), + data, + } + ) + ) + ) + } + }} eventDataOptions={eventDataOptions} customInput={(onChange) => { return ( @@ -66,9 +110,19 @@ const PropertySelect = ({ }} > onChange(data?.data)} + propName={key} + propData={ + val.type === 'literal' + ? val.value + : { + type: propType, + subType, + data: null, + } + } + updateProp={(data) => + onChange(LiteralNode('propData', data)) + } disabled={false} isLoading={false} showIcon={true} @@ -81,7 +135,9 @@ const PropertySelect = ({ @@ -96,10 +152,14 @@ const PropertySelect = ({ event, - onChange({ - ...value, - [prop.name]: { ...prop, data: null }, - }) + addProp( + prop.name, + LiteralNode('propData', { + type: prop.type, + subType: prop.subType, + data: null, + }) + ) } />, @@ -121,13 +181,38 @@ const PropertySelect = ({ export default PropertySelect -function getDataTypeForPropType(type: PropData['type']): string | undefined { +function getDataTypeForPropType( + type: PropData['type'] +): BoostPrimitives | StdPrimitives { switch (type) { case 'number': + case 'status': return 'number' case 'string': - return 'string' default: - return undefined + return 'string' } } + +function getPropTypeFromAst(x: SupportedType['val']) { + if (x.type === 'literal') { + return [x.value.type, x.value.subType] + } + + if ( + x.input.type === 'constructor' && + x.input.info.type === 'struct' && + x.input.info.refs.type != null && + x.input.info.refs.type.type === 'literal' + ) { + return [ + x.input.info.refs.type.value, + x.input.info.refs.subType != null && + x.input.info.refs.subType.type === 'literal' + ? x.input.info.refs.subType.value + : undefined, + ] + } + + return [] +} diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index 9bc115d008..bc34572dab 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -1,13 +1,24 @@ import { mdiFileDocumentOutline } from '@mdi/js' -import React, { useCallback, useEffect, useMemo } from 'react' +import { omit, pickBy } from 'lodash' +import React, { useCallback, useMemo } from 'react' import FormEmoji from '../../../../design/components/molecules/Form/atoms/FormEmoji' import FormInput from '../../../../design/components/molecules/Form/atoms/FormInput' import FormTextarea from '../../../../design/components/molecules/Form/atoms/FormTextArea' import FormRow from '../../../../design/components/molecules/Form/templates/FormRow' -import { flattenObj } from '../../../lib/utils/object' +import { BoostAST } from '../../../lib/automations' +import { + ArrayNode, + LiteralNode, + RecordNode, + StructNode, +} from '../../../lib/automations/ast' +import { flattenType } from '../../../lib/automations/types' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' -import PropertySelect, { PropertySelectProps } from './PropertySelect' +import PropertySelect, { + PropertySelectProps, + SupportedType, +} from './PropertySelect' const UpdateDocActionConfigurator = ({ configuration, @@ -15,76 +26,98 @@ const UpdateDocActionConfigurator = ({ eventType, }: ActionConfiguratorProps) => { const eventDataOptions = useMemo(() => { - return flattenObj(eventType as any) + return Object.fromEntries( + Array.from(flattenType(eventType)).map(([path, type]) => [ + path.join('.'), + type, + ]) + ) }, [eventType]) - const conditions = useMemo(() => { - if (!Array.isArray(configuration.query)) { - return {} + const [propQueryNodes, contentNodes] = useMemo(() => { + if ( + configuration.type !== 'constructor' || + configuration.info.type !== 'struct' + ) { + return [[], {}] } - return configuration.query.reduce( - (acc: Record, condition: any) => { - if (condition.type === 'prop') { - acc[condition.value.name] = { - type: condition.value.type, - data: condition.value.value, - } - } - return acc - }, - {} - ) - }, [configuration.query]) + const propQueryAst = + configuration.info.refs.query != null && + configuration.info.refs.query.type === 'constructor' && + configuration.info.refs.query.info.type === 'array' + ? configuration.info.refs.query.info.refs + : [] + + const contentAst = + configuration.info.refs.content != null && + configuration.info.refs.content.type === 'constructor' && + configuration.info.refs.content.info.type === 'struct' + ? configuration.info.refs.content.info.refs + : {} + + return [propQueryAst, contentAst] + }, [configuration]) + + const propArgs = useMemo(() => { + return propQueryNodes.map(astToPropRef).filter(notNull) + }, [propQueryNodes]) const setContent = useCallback( - (config: Record) => { - onChange({ - ...configuration, - content: { ...(configuration.content || {}), ...config }, - }) + (config: Record) => { + onChange( + StructNode({ + query: ArrayNode(propQueryNodes), + content: StructNode({ + ...omit(contentNodes, Object.keys(config)), + ...pickBy(config, notNull), + }), + }) + ) }, - [configuration, onChange] + [propQueryNodes, contentNodes, onChange] ) const setConditions: PropertySelectProps['onChange'] = useCallback( (props) => { - onChange({ - ...configuration, - query: Object.entries(props).map(([name, prop]) => { - return { - type: 'prop', - value: { name, type: prop.type, value: prop.data }, - rule: 'and', - } - }), - }) + onChange( + StructNode({ + constent: StructNode({ ...contentNodes }), + query: ArrayNode( + props + .map(({ key, val }) => { + return toQueryAST(key, val) + }) + .filter(notNull) + ), + }) + ) }, - [configuration, onChange] + [contentNodes, onChange] ) - useEffect(() => { - console.log(configuration) - }, [configuration]) - return (
setContent({ title })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + value={value?.value} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -92,15 +125,21 @@ const UpdateDocActionConfigurator = ({ setContent({ emoji })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( + onChange( + emoji != null ? LiteralNode('string', emoji) : undefined + ) + } /> ) }} @@ -108,14 +147,18 @@ const UpdateDocActionConfigurator = ({ setContent({ content })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + value={value?.value} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -123,8 +166,14 @@ const UpdateDocActionConfigurator = ({ setContent({ props })} + value={ + contentNodes.props != null && + contentNodes.props.type === 'constructor' && + contentNodes.props.info.type === 'record' + ? (contentNodes.props.info.refs as any) + : [] + } + onChange={(props) => setContent({ props: RecordNode(props) })} eventDataOptions={eventDataOptions} />
@@ -132,3 +181,67 @@ const UpdateDocActionConfigurator = ({ } export default UpdateDocActionConfigurator + +function toQueryAST( + name: SupportedType['key'], + val: SupportedType['val'] +): BoostAST | null { + if ( + val.type === 'operation' && + val.input.type === 'constructor' && + val.input.info.type === 'struct' + ) { + return StructNode({ + type: LiteralNode('string', 'prop'), + value: StructNode({ + name, + type: val.input.info.refs.type, + value: val, + }), + rule: LiteralNode('string', 'and'), + }) + } + + if (val.type === 'literal') { + return StructNode({ + type: LiteralNode('string', 'prop'), + value: StructNode({ + name, + type: LiteralNode('string', val.value.type), + value: LiteralNode('propData', val.value), + }), + rule: LiteralNode('string', 'and'), + }) + } + + return null +} + +function astToPropRef( + ref: BoostAST +): PropertySelectProps['value'][number] | null { + if ( + ref.type === 'constructor' && + ref.info.type === 'struct' && + ref.info.refs.value && + ref.info.refs.value != null && + ref.info.refs.value.type === 'constructor' && + ref.info.refs.value.info.type === 'struct' && + ref.info.refs.value.info.refs.name != null && + ref.info.refs.value.info.refs.name.type === 'literal' && + ref.info.refs.value.info.refs.value != null && + (ref.info.refs.value.info.refs.value.type === 'operation' || + ref.info.refs.value.info.refs.value.type === 'literal') + ) { + return { + key: ref.info.refs.value.info.refs.name, + val: ref.info.refs.value.info.refs.value, + } + } + + return null +} + +function notNull(x: T): x is Exclude { + return x != null +} diff --git a/src/cloud/components/Automations/actions/index.ts b/src/cloud/components/Automations/actions/index.ts index bd7a917915..cd663ca813 100644 --- a/src/cloud/components/Automations/actions/index.ts +++ b/src/cloud/components/Automations/actions/index.ts @@ -1,8 +1,8 @@ import { SerializedPipe } from '../../../interfaces/db/automations' -import { JsonTypeDef } from '../../../lib/automations/events' +import { BoostType } from '../../../lib/automations' export interface ActionConfiguratorProps { - onChange: (conf: SerializedPipe['configuration']) => void - configuration: SerializedPipe['configuration'] - eventType: JsonTypeDef + onChange: (conf: SerializedPipe['configuration']['input']) => void + configuration: SerializedPipe['configuration']['input'] + eventType: BoostType } diff --git a/src/cloud/interfaces/db/automations.ts b/src/cloud/interfaces/db/automations.ts index 7b34711839..5246efae63 100644 --- a/src/cloud/interfaces/db/automations.ts +++ b/src/cloud/interfaces/db/automations.ts @@ -1,9 +1,10 @@ +import { PipeEntry } from '../../lib/automations' + export interface SerializedPipe { name: string - action: string event: string filter?: any - configuration: any + configuration: PipeEntry } export interface SerializedWorkflow { diff --git a/src/cloud/lib/automations/ast.ts b/src/cloud/lib/automations/ast.ts new file mode 100644 index 0000000000..95a59e7cb7 --- /dev/null +++ b/src/cloud/lib/automations/ast.ts @@ -0,0 +1,50 @@ +import { TypeDef, StdPrimitives, Primitive } from './types' + +export type ASTNode

= + | { type: 'operation'; identifier: string; input: ASTNode

} + | { type: 'reference'; identifier: string } + | { type: 'constructor'; info: ConstructorInfo

} + | { + type: 'literal' + def: Extract, { type: 'primitive' }> + value: any + } + +export type ConstructorInfo

= + | { type: 'struct'; refs: Record> } + | { type: 'record'; refs: { key: ASTNode

; val: ASTNode

}[] } + | { type: 'array'; refs: ASTNode

[] } + +export function OpNode

( + identifier: string, + input: ASTNode

+): Extract, { type: 'operation' }> { + return { type: 'operation', identifier, input } +} + +export function RefNode

(identifier: string): ASTNode

{ + return { type: 'reference', identifier } +} + +export function LiteralNode

( + def: P | StdPrimitives, + value: any +): Extract, { type: 'literal' }> { + return { type: 'literal', def: Primitive(def), value } +} + +export function StructNode

( + refs: Record> +): ASTNode

{ + return { type: 'constructor', info: { type: 'struct', refs } } +} + +export function RecordNode

( + refs: { key: ASTNode

; val: ASTNode

}[] +): ASTNode

{ + return { type: 'constructor', info: { type: 'record', refs } } +} + +export function ArrayNode

(refs: ASTNode

[]): ASTNode

{ + return { type: 'constructor', info: { type: 'array', refs } } +} diff --git a/src/cloud/lib/automations/events/index.ts b/src/cloud/lib/automations/events/index.ts index 64974b5a87..480e3a2fe9 100644 --- a/src/cloud/lib/automations/events/index.ts +++ b/src/cloud/lib/automations/events/index.ts @@ -1,55 +1,56 @@ -type JsonPrimitiveTypeTag = 'number' | 'string' | 'boolean' -type JsonArrayTypeTag = `${JsonPrimitiveTypeTag}[]` +import { Struct, Str, Num, Bool, TypeDef } from '../types' -export type JsonTypeDef = - | JsonPrimitiveTypeTag - | JsonArrayTypeTag - | { [key: string]: JsonTypeDef } +const userSchema = Struct({ + login: Str(), + id: Num(), +}) -const abstractGithubIssueEventDef: JsonTypeDef = { - //action: 'string', - issue: { - id: 'number', - number: 'number', - title: 'string', - body: 'string', - state: 'string', - url: 'string', - html_url: 'string', - }, - repository: { - id: 'number', - name: 'string', - full_name: 'string', - url: 'string', - html_url: 'string', - private: 'boolean', - owner: { - login: 'string', - id: 'number', - }, - }, -} +const issueSchema = Struct({ + id: Num(), + number: Num(), + title: Str(), + body: Str(), + state: Str(), + url: Str(), + html_url: Str(), +}) + +const repositorySchema = Struct({ + id: Num(), + name: Str(), + full_name: Str(), + url: Str(), + html_url: Str(), + private: Bool(), + owner: userSchema, +}) + +const labelSchema = Struct({ + id: Num(), + url: Str(), + color: Str(), + name: Str(), +}) -const supportedEvents: Record = { - 'github.issues.opened': abstractGithubIssueEventDef, - 'github.issues.edited': { - ...abstractGithubIssueEventDef, - changes: { - body: { from: 'string' }, - title: { from: 'string' }, - }, - }, - 'github.issues.labeled': { - ...abstractGithubIssueEventDef, - label: { - id: 'number', - url: 'string', - color: 'string', - name: 'string', - default: 'boolean', - }, - }, +const supportedEvents: Record> = { + 'github.issues.opened': Struct({ + issue: issueSchema, + repository: repositorySchema, + }), + 'github.issues.edited': Struct({ + issue: issueSchema, + repository: repositorySchema, + changes: Struct({ + body: Struct({ from: Str() }), + title: Struct({ from: Str() }), + }), + }), + 'github.issues.labeled': Struct({ + action: Str(), + issue: issueSchema, + repository: repositorySchema, + label: labelSchema, + }), } export default supportedEvents diff --git a/src/cloud/lib/automations/index.ts b/src/cloud/lib/automations/index.ts new file mode 100644 index 0000000000..9a175c1bfb --- /dev/null +++ b/src/cloud/lib/automations/index.ts @@ -0,0 +1,7 @@ +import { ASTNode } from './ast' +import { TypeDef } from './types' + +export type BoostPrimitives = 'folder' | 'propData' | 'propDataType' +export type BoostType = TypeDef +export type BoostAST = ASTNode +export type PipeEntry = Extract diff --git a/src/cloud/lib/automations/types.ts b/src/cloud/lib/automations/types.ts new file mode 100644 index 0000000000..85788443d1 --- /dev/null +++ b/src/cloud/lib/automations/types.ts @@ -0,0 +1,85 @@ +export type StdPrimitiveMap = { + string: string + number: number + boolean: boolean +} +export type StdPrimitives = keyof StdPrimitiveMap + +export type TypeDef

= + | { type: 'struct'; def: Record> } + | { type: 'record'; def: TypeDef } + | { type: 'array'; def: TypeDef } + | { type: 'primitive'; def: P | StdPrimitives } + | { type: 'optional'; def: TypeDef } + | (U extends string ? { type: 'reference'; def: U } : never) + +export function Struct>>(def: U) { + return { type: 'struct' as const, def: def as U } +} + +export function Record>(def: T) { + return { type: 'record' as const, def: def as T } +} + +export function Arr>(def: T) { + return { type: 'array' as const, def: def as T } +} + +export function Primitive

(def: P) { + return { type: 'primitive', def } as { type: 'primitive'; def: P } +} + +export function Str() { + return { type: 'primitive' as const, def: 'string' as const } +} + +export function Num() { + return { type: 'primitive' as const, def: 'number' as const } +} + +export function Bool() { + return { type: 'primitive' as const, def: 'boolean' as const } +} + +export function Optional>(def: T) { + return { type: 'optional' as const, def } +} + +export function Reference(def: U) { + return { type: 'reference' as const, def: def as U } +} + +export function* flattenType

( + typeDef: TypeDef, + internalRepr = false +): Generator<[string[], TypeDef]> { + yield [[], typeDef] + + const additionalKey = internalRepr ? ['def'] : [] + switch (typeDef.type) { + case 'struct': { + for (const [key, val] of Object.entries(typeDef.def)) { + for (const [path, nestedType] of flattenType(val, internalRepr)) { + yield [additionalKey.concat([key]).concat(path), nestedType] + } + } + break + } + case 'record': + case 'array': { + for (const [path, nestedType] of flattenType(typeDef.def, internalRepr)) { + yield [(internalRepr ? additionalKey : ['0']).concat(path), nestedType] + } + break + } + case 'optional': { + for (const [path, nestedType] of flattenType(typeDef.def, internalRepr)) { + yield [ + additionalKey.concat(path), + nestedType.type === 'optional' ? nestedType : Optional(nestedType), + ] + } + break + } + } +} diff --git a/src/cloud/pages/workflows/create.tsx b/src/cloud/pages/workflows/create.tsx index 9f323d79a0..3b67f9a302 100644 --- a/src/cloud/pages/workflows/create.tsx +++ b/src/cloud/pages/workflows/create.tsx @@ -2,7 +2,10 @@ import React, { useCallback, useState } from 'react' import { getTeamIndexPageData } from '../../api/pages/teams' import ApplicationContent from '../../components/ApplicationContent' import ApplicationPage from '../../components/ApplicationPage' -import { SerializedWorkflow } from '../../interfaces/db/automations' +import { + SerializedPipe, + SerializedWorkflow, +} from '../../interfaces/db/automations' import Topbar from '../../../design/components/organisms/Topbar' import { useToast } from '../../../design/lib/stores/toast' import { createWorkflow } from '../../api/automation/workflow' @@ -72,17 +75,56 @@ WorkflowCreatePage.getInitialProps = getTeamIndexPageData export default WorkflowCreatePage -const defaultPipe = { +const defaultPipe: SerializedPipe = { name: 'New Pipeline', event: 'github.issues.opened', - action: 'boost.doc.create', configuration: { - title: '$event.issue.title', - content: '$event.issue.body', - props: { - IssueID: { - type: 'number', - data: '$event.issue.id', + type: 'operation', + identifier: 'boost.docs.create', + input: { + type: 'constructor', + info: { + type: 'struct', + refs: { + title: { type: 'reference', identifier: '$event.issue.title' }, + content: { type: 'reference', identifier: '$event.issue.body' }, + props: { + type: 'constructor', + info: { + type: 'record', + refs: [ + { + key: { + type: 'literal', + def: { type: 'primitive', def: 'string' }, + value: 'IssueId', + }, + val: { + type: 'operation', + identifier: 'boost.props.make', + input: { + type: 'constructor', + info: { + type: 'struct', + refs: { + type: { + type: 'literal', + def: { type: 'primitive', def: 'string' }, + value: 'number', + }, + val: { + type: 'reference', + identifier: '$event.issue.id', + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, }, }, },