From d4de6fcffb47395587b8b5665ea082463c8e40cc Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 21 Feb 2022 12:14:08 +0900 Subject: [PATCH 01/10] move in ast types --- .../Automations/WorkflowBuilder.tsx | 16 ++--- src/cloud/interfaces/db/automations.ts | 5 +- src/cloud/lib/automations/ast.ts | 16 +++++ src/cloud/lib/automations/index.ts | 3 + src/cloud/lib/automations/types.ts | 50 ++++++++++++++++ src/cloud/pages/workflows/create.tsx | 60 ++++++++++++++++--- 6 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 src/cloud/lib/automations/ast.ts create mode 100644 src/cloud/lib/automations/index.ts create mode 100644 src/cloud/lib/automations/types.ts 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/interfaces/db/automations.ts b/src/cloud/interfaces/db/automations.ts index 7b34711839..5f38f66951 100644 --- a/src/cloud/interfaces/db/automations.ts +++ b/src/cloud/interfaces/db/automations.ts @@ -1,9 +1,10 @@ +import { BoostAST } from '../../lib/automations' + export interface SerializedPipe { name: string - action: string event: string filter?: any - configuration: any + configuration: BoostAST } 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..e96bbaf06d --- /dev/null +++ b/src/cloud/lib/automations/ast.ts @@ -0,0 +1,16 @@ +import { TypeDef } 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

[] } diff --git a/src/cloud/lib/automations/index.ts b/src/cloud/lib/automations/index.ts new file mode 100644 index 0000000000..c1e8c207e4 --- /dev/null +++ b/src/cloud/lib/automations/index.ts @@ -0,0 +1,3 @@ +import { ASTNode } from './ast' + +export type BoostAST = ASTNode<'folder' | 'propData'> diff --git a/src/cloud/lib/automations/types.ts b/src/cloud/lib/automations/types.ts new file mode 100644 index 0000000000..996e6c7955 --- /dev/null +++ b/src/cloud/lib/automations/types.ts @@ -0,0 +1,50 @@ +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 } +} 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', + }, + }, + }, + }, + }, + }, + ], + }, + }, + }, }, }, }, From e56f473256be2d13cd1008d16b2f55907d1edd45 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Mon, 21 Feb 2022 12:19:20 +0900 Subject: [PATCH 02/10] define triggers in new type system --- src/cloud/lib/automations/events/index.ts | 99 ++++++++++++----------- 1 file changed, 50 insertions(+), 49 deletions(-) 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 From fc0b253f2dfc15e663c292b5792018fcd4684bea Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Tue, 22 Feb 2022 07:47:00 +0900 Subject: [PATCH 03/10] convert create builders --- .../components/Automations/EventInfo.tsx | 1 - .../components/Automations/PipeBuilder.tsx | 29 +++- .../actions/ActionConfigurationInput.tsx | 28 ++-- .../actions/CreateDocActionConfigurator.tsx | 62 +++++-- .../Automations/actions/PropertySelect.tsx | 151 ++++++++++++++---- .../components/Automations/actions/index.ts | 8 +- src/cloud/interfaces/db/automations.ts | 4 +- src/cloud/lib/automations/ast.ts | 36 ++++- src/cloud/lib/automations/index.ts | 6 +- 9 files changed, 248 insertions(+), 77 deletions(-) diff --git a/src/cloud/components/Automations/EventInfo.tsx b/src/cloud/components/Automations/EventInfo.tsx index f3e07b443e..d0ea2864cf 100644 --- a/src/cloud/components/Automations/EventInfo.tsx +++ b/src/cloud/components/Automations/EventInfo.tsx @@ -1,5 +1,4 @@ import React from 'react' -import { JsonTypeDef } from '../../lib/automations/events' interface EventSelectProps { name: string diff --git a/src/cloud/components/Automations/PipeBuilder.tsx b/src/cloud/components/Automations/PipeBuilder.tsx index 6d6801ef1e..a902259bd2 100644 --- a/src/cloud/components/Automations/PipeBuilder.tsx +++ b/src/cloud/components/Automations/PipeBuilder.tsx @@ -34,11 +34,16 @@ 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]) return ( @@ -63,11 +68,7 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { - {currentEvent != null ? ( - - ) : ( -

Select Event
- )} +
Select Event
@@ -96,7 +97,19 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { onChange({ ...pipe, action: value })} + onChange={({ value }) => + onChange({ + ...pipe, + configuration: { + type: 'operation', + identifier: value, + 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..566a8f8e59 100644 --- a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx +++ b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx @@ -2,6 +2,8 @@ import React, { useMemo, useState } 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 } from '../../../lib/automations' +import { RefNode } from '../../../lib/automations/ast' const CONFIG_TYPES = [ { label: 'Event', value: 'event' }, @@ -9,8 +11,8 @@ const CONFIG_TYPES = [ ] interface ActionConfigurationInputProps { - onChange: (value: any) => void - value: any + onChange: (value: BoostAST) => void + value: BoostAST customInput: ( onChange: ActionConfigurationInputProps['onChange'], value: any @@ -26,11 +28,11 @@ const ActionConfigurationInput = ({ type: dataType, }: ActionConfigurationInputProps) => { const [type, setType] = useState(() => { - if (typeof value === 'string') { - if (value.startsWith('$event')) { + 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] } } @@ -44,19 +46,17 @@ const ActionConfigurationInput = ({ }, [eventDataOptions, dataType]) const normalized = useMemo(() => { - if (typeof value === 'string') { - if (value.startsWith('$event')) { - return value.substr('$event.'.length) + 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 ( @@ -69,7 +69,7 @@ const ActionConfigurationInput = ({ onChange(`$event.${value}`)} + onChange={({ value }) => onChange(RefNode(`$event.${value}`))} /> )} {type.value === 'custom' && customInput(onChange, value)} diff --git a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx index b81d3b6b87..74de89c5a5 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -4,12 +4,15 @@ import FormEmoji from '../../../../design/components/molecules/Form/atoms/FormEm 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 { LiteralNode, StructNode } from '../../../lib/automations/ast' import { flattenObj } from '../../../lib/utils/object' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' import FolderSelect from './FolderSelect' import PropertySelect from './PropertySelect' +// TODO: flatten type (bring from backend) ? do top level? 'declarations' || 'imports' +// TODO: sort by type const CreateDocActionConfigurator = ({ configuration, onChange, @@ -19,19 +22,33 @@ const CreateDocActionConfigurator = ({ return flattenObj(eventType as any) }, [eventType]) + const constructorTree = useMemo(() => { + if ( + configuration.type !== 'constructor' || + configuration.info.type !== 'struct' + ) { + return {} + } + return configuration.info.refs + }, [configuration]) + return (
onChange({ ...configuration, title })} + onChange={(title) => + onChange(StructNode({ ...constructorTree, title })) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -39,16 +56,20 @@ const CreateDocActionConfigurator = ({ onChange({ ...configuration, emoji })} + onChange={(emoji) => + onChange(StructNode({ ...constructorTree, emoji })) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( + onChange(LiteralNode('string', emojiStr)) + } /> ) }} @@ -56,15 +77,19 @@ const CreateDocActionConfigurator = ({ onChange({ ...configuration, content })} + onChange={(content) => + onChange(StructNode({ ...constructorTree, content })) + } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -72,21 +97,28 @@ const CreateDocActionConfigurator = ({ - onChange({ ...configuration, parentFolder }) + onChange(StructNode({ ...constructorTree, parentFolder })) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { - return + return ( + onChange(LiteralNode('folder', id))} + /> + ) }} /> onChange({ ...configuration, props })} + value={constructorTree.props || {}} + onChange={(props) => + onChange(StructNode({ ...constructorTree, props })) + } eventDataOptions={eventDataOptions} />
diff --git a/src/cloud/components/Automations/actions/PropertySelect.tsx b/src/cloud/components/Automations/actions/PropertySelect.tsx index 7bbff07255..635f4c20ec 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -1,6 +1,6 @@ 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 +10,25 @@ 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 } from '../../../lib/automations' +import { + LiteralNode, + OpNode, + RecordNode, + StructNode, +} from '../../../lib/automations/ast' export interface PropertySelectProps { - value: Record - onChange: (props: Record) => void + value: BoostAST + onChange: (props: BoostAST) => void eventDataOptions: Record } +type SupportedType = { + key: Extract + val: Extract +} + const PropertySelect = ({ value, onChange, @@ -30,32 +37,76 @@ const PropertySelect = ({ const { openContextModal } = useModal() const props = useMemo(() => { - return Object.entries(value) as [string, SerializedPropData][] + if (value.type !== 'constructor' || value.info.type !== 'record') { + return [] + } + return value.info.refs.filter(isSupported).map((ref) => { + return { key: ref.key.value, val: ref.val } + }) }, [value]) + const addProp = useCallback( + (key: string, val: SupportedType['val']) => { + if (value.type !== 'constructor' || value.info.type !== 'record') { + onChange(RecordNode([{ key: LiteralNode('string', key), val }])) + return + } + + onChange( + RecordNode( + value.info.refs + .filter( + (ref) => ref.key.type === 'literal' && 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) => { + // if literal get type + data from value + // else extrat type from subtype || type + const [propType, subType] = getPropTypeFromAst(val) + const iconPath = getIconPathOfPropType(subType || propType) return ( - + - onChange({ - ...value, - [propName]: { ...propData, data }, - }) - } + value={val} + type={getDataTypeForPropType(propType)} + 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, + } + ) + ) as SupportedType['val'] + ) + } + }} eventDataOptions={eventDataOptions} customInput={(onChange) => { return ( @@ -66,9 +117,11 @@ const PropertySelect = ({ }} > onChange(data?.data)} + propName={key} + propData={val.type === 'literal' ? val.value : null} + updateProp={(data) => + onChange(LiteralNode('propData', data?.data)) + } disabled={false} isLoading={false} showIcon={true} @@ -81,7 +134,7 @@ const PropertySelect = ({ @@ -96,10 +149,14 @@ const PropertySelect = ({ event, - onChange({ - ...value, - [prop.name]: { ...prop, data: null }, - }) + addProp( + prop.name, + LiteralNode('propData', { + type: prop.type, + subType: prop.subType, + data: null, + }) as SupportedType['val'] + ) } />, @@ -131,3 +188,35 @@ function getDataTypeForPropType(type: PropData['type']): string | undefined { return undefined } } + +function isSupported(x: any): x is SupportedType { + return ( + x.key.type === 'literal' && + x.key.def.def === 'string' && + ((x.val.type === 'literal' && x.val.def.def === 'propData') || + (x.val.type === 'operation' && x.val.identifier === 'boost.props.make')) + ) +} + +function getPropTypeFromAst(x: SupportedType['val']) { + if (x.type === 'literal') { + return [x.value.subType, 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/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 5f38f66951..5246efae63 100644 --- a/src/cloud/interfaces/db/automations.ts +++ b/src/cloud/interfaces/db/automations.ts @@ -1,10 +1,10 @@ -import { BoostAST } from '../../lib/automations' +import { PipeEntry } from '../../lib/automations' export interface SerializedPipe { name: string event: string filter?: any - configuration: BoostAST + configuration: PipeEntry } export interface SerializedWorkflow { diff --git a/src/cloud/lib/automations/ast.ts b/src/cloud/lib/automations/ast.ts index e96bbaf06d..530dd2d38c 100644 --- a/src/cloud/lib/automations/ast.ts +++ b/src/cloud/lib/automations/ast.ts @@ -1,4 +1,4 @@ -import { TypeDef } from './types' +import { TypeDef, StdPrimitives, Primitive } from './types' export type ASTNode

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

} @@ -14,3 +14,37 @@ 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

+): ASTNode

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

(identifier: string): ASTNode

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

( + def: P | StdPrimitives, + value: any +): ASTNode

{ + 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/index.ts b/src/cloud/lib/automations/index.ts index c1e8c207e4..0d9ff7d90b 100644 --- a/src/cloud/lib/automations/index.ts +++ b/src/cloud/lib/automations/index.ts @@ -1,3 +1,7 @@ import { ASTNode } from './ast' +import { TypeDef } from './types' -export type BoostAST = ASTNode<'folder' | 'propData'> +export type BoostPrimitives = 'folder' | 'propData' +export type BoostType = TypeDef +export type BoostAST = ASTNode +export type PipeEntry = Extract From cae753f36f2b1ad2b38bfd917b6912a701321db3 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Tue, 22 Feb 2022 11:12:46 +0900 Subject: [PATCH 04/10] action configuration conversion --- .../components/Automations/EventInfo.tsx | 3 +- .../components/Automations/FilterBuilder.tsx | 4 +- .../components/Automations/PipeBuilder.tsx | 28 +-- .../actions/CreateDocActionConfigurator.tsx | 31 ++- .../Automations/actions/PropertySelect.tsx | 44 +--- .../actions/UpdateDocActionConfigurator.tsx | 192 +++++++++++++----- src/cloud/lib/automations/ast.ts | 4 +- src/cloud/lib/automations/index.ts | 2 +- 8 files changed, 198 insertions(+), 110 deletions(-) diff --git a/src/cloud/components/Automations/EventInfo.tsx b/src/cloud/components/Automations/EventInfo.tsx index d0ea2864cf..cace3f41d2 100644 --- a/src/cloud/components/Automations/EventInfo.tsx +++ b/src/cloud/components/Automations/EventInfo.tsx @@ -1,8 +1,9 @@ import React from 'react' +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..8ae31fd35b 100644 --- a/src/cloud/components/Automations/FilterBuilder.tsx +++ b/src/cloud/components/Automations/FilterBuilder.tsx @@ -10,11 +10,11 @@ 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 { flattenObj } from '../../lib/utils/object' interface FilterBuilderProps { - typeDef: JsonTypeDef + typeDef: BoostType filter: SerializedPipe['filter'] onChange: (filter: SerializedPipe['filter']) => void } diff --git a/src/cloud/components/Automations/PipeBuilder.tsx b/src/cloud/components/Automations/PipeBuilder.tsx index a902259bd2..2ea138e13f 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) => { @@ -45,6 +45,13 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { ) }, [pipe.configuration]) + const updateConfig = useCallback( + (input: SerializedPipe['configuration']['input']) => { + onChange({ ...pipe, configuration: { ...pipe.configuration, input } }) + }, + [pipe, onChange] + ) + return ( @@ -100,14 +107,7 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { onChange={({ value }) => onChange({ ...pipe, - configuration: { - type: 'operation', - identifier: value, - input: { - type: 'constructor', - info: { type: 'struct', refs: {} }, - }, - }, + configuration: OpNode(value, StructNode({})), }) } /> @@ -116,15 +116,15 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { {action.value === 'boost.doc.create' && ( onChange({ ...pipe, configuration })} + configuration={pipe.configuration.input} + onChange={updateConfig} eventType={currentEvent} /> )} {action.value === 'boost.doc.update' && ( onChange({ ...pipe, configuration })} + configuration={pipe.configuration.input} + onChange={updateConfig} eventType={currentEvent} /> )} diff --git a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx index 74de89c5a5..b5e16afcc7 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -4,15 +4,18 @@ import FormEmoji from '../../../../design/components/molecules/Form/atoms/FormEm 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 { LiteralNode, StructNode } from '../../../lib/automations/ast' +import { BoostAST } from '../../../lib/automations' +import { + LiteralNode, + RecordNode, + StructNode, +} from '../../../lib/automations/ast' import { flattenObj } from '../../../lib/utils/object' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' import FolderSelect from './FolderSelect' -import PropertySelect from './PropertySelect' +import PropertySelect, { SupportedType } from './PropertySelect' -// TODO: flatten type (bring from backend) ? do top level? 'declarations' || 'imports' -// TODO: sort by type const CreateDocActionConfigurator = ({ configuration, onChange, @@ -115,9 +118,15 @@ const CreateDocActionConfigurator = ({ - onChange(StructNode({ ...constructorTree, props })) + onChange(StructNode({ ...constructorTree, props: RecordNode(props) })) } eventDataOptions={eventDataOptions} /> @@ -125,4 +134,14 @@ const CreateDocActionConfigurator = ({ ) } +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 635f4c20ec..b08c4f5e63 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -11,20 +11,15 @@ import ActionConfigurationInput from './ActionConfigurationInput' import { useModal } from '../../../../design/lib/stores/modal' import { PropData } from '../../../interfaces/db/props' import { BoostAST } from '../../../lib/automations' -import { - LiteralNode, - OpNode, - RecordNode, - StructNode, -} from '../../../lib/automations/ast' +import { LiteralNode, OpNode, StructNode } from '../../../lib/automations/ast' export interface PropertySelectProps { - value: BoostAST - onChange: (props: BoostAST) => void + value: SupportedType[] + onChange: (props: SupportedType[]) => void eventDataOptions: Record } -type SupportedType = { +export type SupportedType = { key: Extract val: Extract } @@ -37,29 +32,17 @@ const PropertySelect = ({ const { openContextModal } = useModal() const props = useMemo(() => { - if (value.type !== 'constructor' || value.info.type !== 'record') { - return [] - } - return value.info.refs.filter(isSupported).map((ref) => { + return value.map((ref) => { return { key: ref.key.value, val: ref.val } }) }, [value]) const addProp = useCallback( (key: string, val: SupportedType['val']) => { - if (value.type !== 'constructor' || value.info.type !== 'record') { - onChange(RecordNode([{ key: LiteralNode('string', key), val }])) - return - } - onChange( - RecordNode( - value.info.refs - .filter( - (ref) => ref.key.type === 'literal' && key !== ref.key.value - ) - .concat([{ key: LiteralNode('string', key), val }]) - ) + value + .filter((ref) => key !== ref.key.value) + .concat([{ key: LiteralNode('string', key), val }]) ) }, [value, onChange] @@ -68,8 +51,6 @@ const PropertySelect = ({ return ( <> {props.map(({ key, val }, i) => { - // if literal get type + data from value - // else extrat type from subtype || type const [propType, subType] = getPropTypeFromAst(val) const iconPath = getIconPathOfPropType(subType || propType) return ( @@ -189,15 +170,6 @@ function getDataTypeForPropType(type: PropData['type']): string | undefined { } } -function isSupported(x: any): x is SupportedType { - return ( - x.key.type === 'literal' && - x.key.def.def === 'string' && - ((x.val.type === 'literal' && x.val.def.def === 'propData') || - (x.val.type === 'operation' && x.val.identifier === 'boost.props.make')) - ) -} - function getPropTypeFromAst(x: SupportedType['val']) { if (x.type === 'literal') { return [x.value.subType, x.value.subType] diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index 9bc115d008..e56e56ac57 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -1,13 +1,23 @@ import { mdiFileDocumentOutline } from '@mdi/js' -import React, { useCallback, useEffect, useMemo } from 'react' +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 { BoostAST } from '../../../lib/automations' +import { + ArrayNode, + LiteralNode, + RecordNode, + StructNode, +} from '../../../lib/automations/ast' import { flattenObj } from '../../../lib/utils/object' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' -import PropertySelect, { PropertySelectProps } from './PropertySelect' +import PropertySelect, { + PropertySelectProps, + SupportedType, +} from './PropertySelect' const UpdateDocActionConfigurator = ({ configuration, @@ -18,73 +28,88 @@ const UpdateDocActionConfigurator = ({ return flattenObj(eventType as any) }, [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({ + ...contentNodes, + ...config, + }), + }) + ) }, - [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)} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -92,7 +117,7 @@ const UpdateDocActionConfigurator = ({ setContent({ emoji })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { @@ -100,7 +125,7 @@ const UpdateDocActionConfigurator = ({ onChange(LiteralNode('string', emoji))} /> ) }} @@ -108,14 +133,16 @@ const UpdateDocActionConfigurator = ({ setContent({ content })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(ev.target.value)} + onChange={(ev) => + onChange(LiteralNode('string', ev.target.value)) + } /> ) }} @@ -123,8 +150,13 @@ const UpdateDocActionConfigurator = ({ setContent({ props })} + value={ + 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 +164,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.input.info.refs.data, + }), + 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.data), + }), + 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/lib/automations/ast.ts b/src/cloud/lib/automations/ast.ts index 530dd2d38c..95a59e7cb7 100644 --- a/src/cloud/lib/automations/ast.ts +++ b/src/cloud/lib/automations/ast.ts @@ -18,7 +18,7 @@ export type ConstructorInfo

= export function OpNode

( identifier: string, input: ASTNode

-): ASTNode

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

(identifier: string): ASTNode

{ export function LiteralNode

( def: P | StdPrimitives, value: any -): ASTNode

{ +): Extract, { type: 'literal' }> { return { type: 'literal', def: Primitive(def), value } } diff --git a/src/cloud/lib/automations/index.ts b/src/cloud/lib/automations/index.ts index 0d9ff7d90b..9a175c1bfb 100644 --- a/src/cloud/lib/automations/index.ts +++ b/src/cloud/lib/automations/index.ts @@ -1,7 +1,7 @@ import { ASTNode } from './ast' import { TypeDef } from './types' -export type BoostPrimitives = 'folder' | 'propData' +export type BoostPrimitives = 'folder' | 'propData' | 'propDataType' export type BoostType = TypeDef export type BoostAST = ASTNode export type PipeEntry = Extract From 44935dcf94812649a20e9f9df9943845071f1fb0 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Tue, 22 Feb 2022 13:40:20 +0900 Subject: [PATCH 05/10] null bug fixes --- .../actions/ActionConfigurationInput.tsx | 47 +++++++++++++++---- .../actions/CreateDocActionConfigurator.tsx | 16 ++++--- .../Automations/actions/PropertySelect.tsx | 44 +++++++++++++---- .../actions/UpdateDocActionConfigurator.tsx | 12 +++-- 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx index 566a8f8e59..106d0eb9d2 100644 --- a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx +++ b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx @@ -1,9 +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 } from '../../../lib/automations' -import { RefNode } from '../../../lib/automations/ast' +import { BoostAST, BoostPrimitives } from '../../../lib/automations' +import { LiteralNode, RefNode } from '../../../lib/automations/ast' +import { StdPrimitives } from '../../../lib/automations/types' const CONFIG_TYPES = [ { label: 'Event', value: 'event' }, @@ -15,10 +16,11 @@ interface ActionConfigurationInputProps { value: BoostAST customInput: ( onChange: ActionConfigurationInputProps['onChange'], - value: any + value: Extract | null ) => React.ReactNode eventDataOptions: Record - type?: string + type: BoostPrimitives | StdPrimitives + defaultValue: any } const ActionConfigurationInput = ({ value, @@ -26,8 +28,13 @@ const ActionConfigurationInput = ({ onChange, customInput, type: dataType, + defaultValue, }: ActionConfigurationInputProps) => { - const [type, setType] = useState(() => { + const type = useMemo(() => { + if (value == null) { + return CONFIG_TYPES[1] + } + if (value.type === 'reference') { if (value.identifier.startsWith('$event')) { return CONFIG_TYPES[0] @@ -37,7 +44,7 @@ const ActionConfigurationInput = ({ } } return CONFIG_TYPES[1] - }) + }, [value]) const options = useMemo(() => { return Object.keys( @@ -46,6 +53,10 @@ const ActionConfigurationInput = ({ }, [eventDataOptions, dataType]) const normalized = useMemo(() => { + if (value == null) { + return '' + } + if (value.type === 'reference') { if (value.identifier.startsWith('$event')) { return value.identifier.substr('$event.'.length) @@ -56,13 +67,23 @@ const ActionConfigurationInput = ({ } } - return value.type === 'literal' ? value.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' && ( @@ -72,7 +93,13 @@ const ActionConfigurationInput = ({ 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 b5e16afcc7..c8b91cc89b 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -41,6 +41,7 @@ const CreateDocActionConfigurator = ({ onChange(StructNode({ ...constructorTree, title })) } @@ -48,7 +49,7 @@ const CreateDocActionConfigurator = ({ customInput={(onChange, value) => { return ( onChange(LiteralNode('string', ev.target.value)) } @@ -60,7 +61,8 @@ const CreateDocActionConfigurator = ({ onChange(StructNode({ ...constructorTree, emoji })) } @@ -68,7 +70,7 @@ const CreateDocActionConfigurator = ({ customInput={(onChange, value) => { return ( onChange(LiteralNode('string', emojiStr)) @@ -81,7 +83,8 @@ const CreateDocActionConfigurator = ({ onChange(StructNode({ ...constructorTree, content })) } @@ -89,7 +92,7 @@ const CreateDocActionConfigurator = ({ customInput={(onChange, value) => { return ( onChange(LiteralNode('string', ev.target.value)) } @@ -102,6 +105,7 @@ const CreateDocActionConfigurator = ({ onChange(StructNode({ ...constructorTree, parentFolder })) } @@ -109,7 +113,7 @@ const CreateDocActionConfigurator = ({ customInput={(onChange, value) => { return ( onChange(LiteralNode('folder', id))} /> ) diff --git a/src/cloud/components/Automations/actions/PropertySelect.tsx b/src/cloud/components/Automations/actions/PropertySelect.tsx index b08c4f5e63..287f8126d7 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -1,4 +1,3 @@ -import { dissoc } from 'ramda' import { mdiClose, mdiPlus } from '@mdi/js' import React, { useCallback, useMemo } from 'react' import Button from '../../../../design/components/atoms/Button' @@ -10,8 +9,9 @@ import PropRegisterCreationForm from '../../Props/PropRegisterModal/PropRegister import ActionConfigurationInput from './ActionConfigurationInput' import { useModal } from '../../../../design/lib/stores/modal' import { PropData } from '../../../interfaces/db/props' -import { BoostAST } from '../../../lib/automations' +import { BoostAST, BoostPrimitives } from '../../../lib/automations' import { LiteralNode, OpNode, StructNode } from '../../../lib/automations/ast' +import { StdPrimitives } from '../../../lib/automations/types' export interface PropertySelectProps { value: SupportedType[] @@ -62,8 +62,20 @@ const PropertySelect = ({ { if (data.type === 'literal') { addProp(key, data) @@ -99,9 +111,17 @@ const PropertySelect = ({ > - onChange(LiteralNode('propData', data?.data)) + onChange(LiteralNode('propData', data)) } disabled={false} isLoading={false} @@ -115,7 +135,9 @@ const PropertySelect = ({ @@ -159,20 +181,22 @@ 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.subType, x.value.subType] + return [x.value.type, x.value.subType] } if ( diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index e56e56ac57..df874ad306 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -101,12 +101,14 @@ const UpdateDocActionConfigurator = ({ setContent({ title })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(LiteralNode('string', ev.target.value)) } @@ -118,12 +120,14 @@ const UpdateDocActionConfigurator = ({ setContent({ emoji })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(LiteralNode('string', emoji))} /> @@ -134,12 +138,14 @@ const UpdateDocActionConfigurator = ({ setContent({ content })} eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(LiteralNode('string', ev.target.value)) } From 19eb778ceefa6833f0a400df6f78f5ba3bad6547 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Tue, 22 Feb 2022 13:51:30 +0900 Subject: [PATCH 06/10] correct type flattening --- .../actions/ActionConfigurationInput.tsx | 9 +++-- .../actions/CreateDocActionConfigurator.tsx | 9 +++-- .../Automations/actions/PropertySelect.tsx | 2 +- .../actions/UpdateDocActionConfigurator.tsx | 9 +++-- src/cloud/lib/automations/types.ts | 35 +++++++++++++++++++ 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx index 106d0eb9d2..6793e765c7 100644 --- a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx +++ b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx @@ -2,7 +2,7 @@ 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 } from '../../../lib/automations' +import { BoostAST, BoostPrimitives, BoostType } from '../../../lib/automations' import { LiteralNode, RefNode } from '../../../lib/automations/ast' import { StdPrimitives } from '../../../lib/automations/types' @@ -18,7 +18,7 @@ interface ActionConfigurationInputProps { onChange: ActionConfigurationInputProps['onChange'], value: Extract | null ) => React.ReactNode - eventDataOptions: Record + eventDataOptions: Record type: BoostPrimitives | StdPrimitives defaultValue: any } @@ -48,7 +48,10 @@ const ActionConfigurationInput = ({ 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]) diff --git a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx index c8b91cc89b..c3cebe00ab 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -10,7 +10,7 @@ import { RecordNode, StructNode, } from '../../../lib/automations/ast' -import { flattenObj } from '../../../lib/utils/object' +import { flattenType } from '../../../lib/automations/types' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' import FolderSelect from './FolderSelect' @@ -22,7 +22,12 @@ 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(() => { diff --git a/src/cloud/components/Automations/actions/PropertySelect.tsx b/src/cloud/components/Automations/actions/PropertySelect.tsx index 287f8126d7..f8c580e0e5 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -96,7 +96,7 @@ const PropertySelect = ({ data, } ) - ) as SupportedType['val'] + ) ) } }} diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index df874ad306..c02da39977 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -11,7 +11,7 @@ import { RecordNode, StructNode, } from '../../../lib/automations/ast' -import { flattenObj } from '../../../lib/utils/object' +import { flattenType } from '../../../lib/automations/types' import { ActionConfiguratorProps } from './' import ActionConfigurationInput from './ActionConfigurationInput' import PropertySelect, { @@ -25,7 +25,12 @@ 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 [propQueryNodes, contentNodes] = useMemo(() => { diff --git a/src/cloud/lib/automations/types.ts b/src/cloud/lib/automations/types.ts index 996e6c7955..85788443d1 100644 --- a/src/cloud/lib/automations/types.ts +++ b/src/cloud/lib/automations/types.ts @@ -48,3 +48,38 @@ export function Optional>(def: T) { 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 + } + } +} From 34c86f40534a41a425d00ccd1e12b13d3b14b69b Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Thu, 24 Feb 2022 10:15:55 +0900 Subject: [PATCH 07/10] bug fixes --- .../components/Automations/actions/PropertySelect.tsx | 2 +- .../actions/UpdateDocActionConfigurator.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cloud/components/Automations/actions/PropertySelect.tsx b/src/cloud/components/Automations/actions/PropertySelect.tsx index f8c580e0e5..1e747f4f87 100644 --- a/src/cloud/components/Automations/actions/PropertySelect.tsx +++ b/src/cloud/components/Automations/actions/PropertySelect.tsx @@ -158,7 +158,7 @@ const PropertySelect = ({ type: prop.type, subType: prop.subType, data: null, - }) as SupportedType['val'] + }) ) } />, diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index c02da39977..6181f24c5e 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -42,14 +42,14 @@ const UpdateDocActionConfigurator = ({ } const propQueryAst = - configuration.info.refs.query !== null && + 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 != null && configuration.info.refs.content.type === 'constructor' && configuration.info.refs.content.info.type === 'struct' ? configuration.info.refs.content.info.refs @@ -162,10 +162,11 @@ const UpdateDocActionConfigurator = ({ setContent({ props: RecordNode(props) })} eventDataOptions={eventDataOptions} @@ -190,7 +191,7 @@ function toQueryAST( value: StructNode({ name, type: val.input.info.refs.type, - value: val.input.info.refs.data, + value: val, }), rule: LiteralNode('string', 'and'), }) @@ -202,7 +203,7 @@ function toQueryAST( value: StructNode({ name, type: LiteralNode('string', val.value.type), - value: LiteralNode('propData', val.value.data), + value: LiteralNode('propData', val.value), }), rule: LiteralNode('string', 'and'), }) From b9817b7eb68c1f0a39a44e6c8891bf31b1a5b0a2 Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Thu, 24 Feb 2022 11:13:52 +0900 Subject: [PATCH 08/10] correct operation names --- src/cloud/components/Automations/PipeBuilder.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cloud/components/Automations/PipeBuilder.tsx b/src/cloud/components/Automations/PipeBuilder.tsx index 2ea138e13f..8a3ebf7bcc 100644 --- a/src/cloud/components/Automations/PipeBuilder.tsx +++ b/src/cloud/components/Automations/PipeBuilder.tsx @@ -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 { @@ -114,14 +114,14 @@ const PipeBuilder = ({ pipe, onChange }: PipeBuilderProps) => { - {action.value === 'boost.doc.create' && ( + {action.value === 'boost.docs.create' && ( )} - {action.value === 'boost.doc.update' && ( + {action.value === 'boost.docs.update' && ( Date: Fri, 25 Feb 2022 09:51:03 +0900 Subject: [PATCH 09/10] fix filter event data flattening --- .../components/Automations/FilterBuilder.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/cloud/components/Automations/FilterBuilder.tsx b/src/cloud/components/Automations/FilterBuilder.tsx index 8ae31fd35b..85ec3193d4 100644 --- a/src/cloud/components/Automations/FilterBuilder.tsx +++ b/src/cloud/components/Automations/FilterBuilder.tsx @@ -11,6 +11,7 @@ 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 { BoostType } from '../../lib/automations' +import { flattenType } from '../../lib/automations/types' import { flattenObj } from '../../lib/utils/object' interface FilterBuilderProps { @@ -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]) From 4cf7c885bf5ce2add65f82c5182cf3fa99ca725f Mon Sep 17 00:00:00 2001 From: Simon Leigh Date: Fri, 25 Feb 2022 11:35:51 +0900 Subject: [PATCH 10/10] clearable config --- .../actions/ActionConfigurationInput.tsx | 2 +- .../actions/CreateDocActionConfigurator.tsx | 43 ++++++++++++++++--- .../actions/UpdateDocActionConfigurator.tsx | 13 ++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx index 6793e765c7..944232261e 100644 --- a/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx +++ b/src/cloud/components/Automations/actions/ActionConfigurationInput.tsx @@ -12,7 +12,7 @@ const CONFIG_TYPES = [ ] interface ActionConfigurationInputProps { - onChange: (value: BoostAST) => void + onChange: (value: BoostAST | undefined) => void value: BoostAST customInput: ( onChange: ActionConfigurationInputProps['onChange'], diff --git a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx index c3cebe00ab..c6652f2a52 100644 --- a/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/CreateDocActionConfigurator.tsx @@ -1,4 +1,5 @@ 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' @@ -48,7 +49,13 @@ const CreateDocActionConfigurator = ({ type={'string'} defaultValue='' onChange={(title) => - onChange(StructNode({ ...constructorTree, title })) + onChange( + StructNode( + title != null + ? { ...constructorTree, title } + : dissoc('title', title) + ) + ) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { @@ -69,7 +76,13 @@ const CreateDocActionConfigurator = ({ type='string' defaultValue='' onChange={(emoji) => - onChange(StructNode({ ...constructorTree, emoji })) + onChange( + StructNode( + emoji != null + ? { ...constructorTree, emoji } + : dissoc('emoji', constructorTree) + ) + ) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { @@ -78,7 +91,11 @@ const CreateDocActionConfigurator = ({ emoji={value?.value} defaultIcon={mdiFileDocumentOutline} setEmoji={(emojiStr) => - onChange(LiteralNode('string', emojiStr)) + onChange( + emojiStr != null + ? LiteralNode('string', emojiStr) + : undefined + ) } /> ) @@ -91,7 +108,13 @@ const CreateDocActionConfigurator = ({ type='string' defaultValue='' onChange={(content) => - onChange(StructNode({ ...constructorTree, content })) + onChange( + StructNode( + content != null + ? { ...constructorTree, content } + : dissoc('content', constructorTree) + ) + ) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { @@ -112,14 +135,22 @@ const CreateDocActionConfigurator = ({ type={'folder'} defaultValue={undefined} onChange={(parentFolder) => - onChange(StructNode({ ...constructorTree, parentFolder })) + onChange( + StructNode( + parentFolder != null + ? { ...constructorTree, parentFolder } + : dissoc('parentFolder', constructorTree) + ) + ) } eventDataOptions={eventDataOptions} customInput={(onChange, value) => { return ( onChange(LiteralNode('folder', id))} + onChange={(id) => + onChange(id != null ? LiteralNode('folder', id) : undefined) + } /> ) }} diff --git a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx index 6181f24c5e..bc34572dab 100644 --- a/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx +++ b/src/cloud/components/Automations/actions/UpdateDocActionConfigurator.tsx @@ -1,4 +1,5 @@ import { mdiFileDocumentOutline } from '@mdi/js' +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' @@ -63,13 +64,13 @@ const UpdateDocActionConfigurator = ({ }, [propQueryNodes]) const setContent = useCallback( - (config: Record) => { + (config: Record) => { onChange( StructNode({ query: ArrayNode(propQueryNodes), content: StructNode({ - ...contentNodes, - ...config, + ...omit(contentNodes, Object.keys(config)), + ...pickBy(config, notNull), }), }) ) @@ -134,7 +135,11 @@ const UpdateDocActionConfigurator = ({ onChange(LiteralNode('string', emoji))} + setEmoji={(emoji) => + onChange( + emoji != null ? LiteralNode('string', emoji) : undefined + ) + } /> ) }}