From 8512528fcf94325a1b831d3caf4b249d8ff36a6d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 15 Mar 2022 02:57:37 +0300 Subject: [PATCH] feat: Implement getData query and send a message action for slack --- packages/backend/src/apps/slack/data.ts | 10 + .../src/apps/slack/data/list-channels.ts | 21 ++ packages/backend/src/apps/slack/index.ts | 3 + packages/backend/src/apps/slack/info.json | 53 +++++ packages/backend/src/apps/twitter/info.json | 8 +- .../backend/src/graphql/queries/get-data.ts | 31 +++ .../backend/src/graphql/query-resolvers.ts | 2 + packages/backend/src/graphql/schema.graphql | 29 ++- packages/backend/src/models/execution-step.ts | 1 + packages/backend/src/services/processor.ts | 10 + packages/types/index.d.ts | 27 ++- .../web/src/components/FlowStep/index.tsx | 190 +++++++++++------- packages/web/src/graphql/queries/get-app.ts | 6 +- packages/web/src/graphql/queries/get-apps.ts | 6 +- 14 files changed, 305 insertions(+), 92 deletions(-) create mode 100644 packages/backend/src/apps/slack/data.ts create mode 100644 packages/backend/src/apps/slack/data/list-channels.ts create mode 100644 packages/backend/src/graphql/queries/get-data.ts diff --git a/packages/backend/src/apps/slack/data.ts b/packages/backend/src/apps/slack/data.ts new file mode 100644 index 00000000..ed4c9bf6 --- /dev/null +++ b/packages/backend/src/apps/slack/data.ts @@ -0,0 +1,10 @@ +import ListChannels from './data/list-channels'; +import { IJSONObject } from '@automatisch/types'; + +export default class Data { + listChannels: ListChannels; + + constructor(connectionData: IJSONObject) { + this.listChannels = new ListChannels(connectionData); + } +} diff --git a/packages/backend/src/apps/slack/data/list-channels.ts b/packages/backend/src/apps/slack/data/list-channels.ts new file mode 100644 index 00000000..36d0ea8e --- /dev/null +++ b/packages/backend/src/apps/slack/data/list-channels.ts @@ -0,0 +1,21 @@ +import type { IJSONObject } from '@automatisch/types'; +import { WebClient } from '@slack/web-api'; + +export default class ListChannels { + client: WebClient; + + constructor(connectionData: IJSONObject) { + this.client = new WebClient(connectionData.accessToken as string); + } + + async run() { + const { channels } = await this.client.conversations.list(); + + return channels.map((channel) => { + return { + value: channel.id, + name: channel.name, + }; + }); + } +} diff --git a/packages/backend/src/apps/slack/index.ts b/packages/backend/src/apps/slack/index.ts index 16c16021..93f2c6f0 100644 --- a/packages/backend/src/apps/slack/index.ts +++ b/packages/backend/src/apps/slack/index.ts @@ -1,4 +1,5 @@ import Authentication from './authentication'; +import Data from './data'; import { IService, IAuthentication, @@ -8,8 +9,10 @@ import { export default class Slack implements IService { authenticationClient: IAuthentication; + data: Data; constructor(appData: IApp, connectionData: IJSONObject) { this.authenticationClient = new Authentication(appData, connectionData); + this.data = new Data(connectionData); } } diff --git a/packages/backend/src/apps/slack/info.json b/packages/backend/src/apps/slack/info.json index c979f604..b8d57e35 100644 --- a/packages/backend/src/apps/slack/info.json +++ b/packages/backend/src/apps/slack/info.json @@ -96,5 +96,58 @@ } ] } + ], + "actions": [ + { + "name": "Send a message to channel", + "key": "sendMessageToChannel", + "description": "Send a message to a specific channel you specify.", + "substeps": [ + { + "key": "chooseAccount", + "name": "Choose account" + }, + { + "key": "setupAction", + "name": "Set up action", + "arguments": [ + { + "label": "Channel", + "key": "channel", + "type": "dropdown", + "required": true, + "description": "Pick a channel to send the message to.", + "variables": false, + "source": { + "type": "query", + "name": "getData", + "arguments": [ + { + "name": "stepId", + "value": "{step.id}" + }, + { + "name": "key", + "value": "listChannels" + } + ] + } + }, + { + "label": "Message text", + "key": "message", + "type": "string", + "required": true, + "description": "The content of your new message.", + "variables": true + } + ] + }, + { + "key": "testStep", + "name": "Test action" + } + ] + } ] } diff --git a/packages/backend/src/apps/twitter/info.json b/packages/backend/src/apps/twitter/info.json index 74a46ae3..0c4c2746 100644 --- a/packages/backend/src/apps/twitter/info.json +++ b/packages/backend/src/apps/twitter/info.json @@ -221,7 +221,7 @@ "key": "myTweet", "interval": "15m", "description": "Will be triggered when you tweet something new.", - "subSteps": [ + "substeps": [ { "key": "chooseAccount", "name": "Choose account" @@ -236,7 +236,7 @@ "name": "User Tweet", "key": "userTweet", "description": "Will be triggered when a specific user tweet something new.", - "subSteps": [ + "substeps": [ { "key": "chooseAccount", "name": "Choose account" @@ -263,7 +263,7 @@ "name": "Search Tweet", "key": "searchTweet", "description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.", - "subSteps": [ + "substeps": [ { "key": "chooseAccount", "name": "Choose account" @@ -292,7 +292,7 @@ "name": "Create Tweet", "key": "createTweet", "description": "Will create a tweet.", - "subSteps": [ + "substeps": [ { "key": "chooseAccount", "name": "Choose account" diff --git a/packages/backend/src/graphql/queries/get-data.ts b/packages/backend/src/graphql/queries/get-data.ts new file mode 100644 index 00000000..ec7a426a --- /dev/null +++ b/packages/backend/src/graphql/queries/get-data.ts @@ -0,0 +1,31 @@ +import App from '../../models/app'; +import Connection from '../../models/connection'; +import Step from '../../models/step'; +import { IApp } from '@automatisch/types'; +import Context from '../../types/express/context'; +import ListData from '../../apps/slack/data/list-channels'; + +type Params = { + stepId: string; + key: string; +}; + +const getData = async (_parent: unknown, params: Params, context: Context) => { + const step = await context.currentUser + .$relatedQuery('steps') + .withGraphFetched('connection') + .findById(params.stepId); + + const connection = step.connection; + + const appData = App.findOneByKey(step.appKey); + const AppClass = (await import(`../../apps/${step.appKey}`)).default; + + const appInstance = new AppClass(appData, connection.formattedData); + const command = appInstance.data[params.key]; + const fetchedData = await command.run(); + + return fetchedData; +}; + +export default getData; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index d1ee3aba..0c06219e 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -8,6 +8,7 @@ import getFlows from './queries/get-flows'; import getStepWithTestExecutions from './queries/get-step-with-test-executions'; import getExecutions from './queries/get-executions'; import getExecutionSteps from './queries/get-execution-steps'; +import getData from './queries/get-data'; const queryResolvers = { getApps, @@ -20,6 +21,7 @@ const queryResolvers = { getStepWithTestExecutions, getExecutions, getExecutionSteps, + getData, }; export default queryResolvers; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index bed035ab..34c04984 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -13,6 +13,7 @@ type Query { limit: Int! offset: Int! ): ExecutionStepConnection + getData(stepId: String!, key: String!): JSONObject } type Mutation { @@ -48,22 +49,34 @@ type Action { name: String key: String description: String - subSteps: [ActionSubStep] + substeps: [ActionSubstep] } -type ActionSubStep { +type ActionSubstep { key: String name: String - arguments: [ActionSubStepArgument] + arguments: [ActionSubstepArgument] } -type ActionSubStepArgument { +type ActionSubstepArgument { label: String key: String type: String description: String required: Boolean variables: Boolean + source: ActionSubstepArgumentSource +} + +type ActionSubstepArgumentSource { + type: String + name: String + arguments: [ActionSubstepArgumentSourceArgument] +} + +type ActionSubstepArgumentSourceArgument { + name: String + value: String } type App { @@ -337,16 +350,16 @@ type Trigger { name: String key: String description: String - subSteps: [TriggerSubStep] + substeps: [TriggerSubstep] } -type TriggerSubStep { +type TriggerSubstep { key: String name: String - arguments: [TriggerSubStepArgument] + arguments: [TriggerSubstepArgument] } -type TriggerSubStepArgument { +type TriggerSubstepArgument { label: String key: String type: String diff --git a/packages/backend/src/models/execution-step.ts b/packages/backend/src/models/execution-step.ts index 0ce135a9..cebcd754 100644 --- a/packages/backend/src/models/execution-step.ts +++ b/packages/backend/src/models/execution-step.ts @@ -9,6 +9,7 @@ class ExecutionStep extends Base { dataIn!: Record; dataOut!: Record; status: string; + step: Step; static tableName = 'execution_steps'; diff --git a/packages/backend/src/services/processor.ts b/packages/backend/src/services/processor.ts index 2e0af63c..b0fce150 100644 --- a/packages/backend/src/services/processor.ts +++ b/packages/backend/src/services/processor.ts @@ -78,6 +78,16 @@ class Processor { fetchedActionData = await command.run(); } + console.log( + 'previous execution step dataOut :', + previousExecutionStep?.dataOut + ); + + console.log( + 'previous execution step parameters :', + previousExecutionStep?.step.parameters + ); + previousExecutionStep = await execution .$relatedQuery('executionSteps') .insertAndFetch({ diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index c9f1a624..905c6715 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -70,7 +70,30 @@ export interface IUser { steps: IStep[]; } -export interface IField { +export interface IFieldDropdown { + key: string; + label: string; + type: 'dropdown'; + required: boolean; + readOnly: boolean; + value: string; + placeholder: string | null; + description: string; + docUrl: string; + clickToCopy: boolean; + name: string; + variables: boolean; + source: { + type: string; + name: string; + arguments: { + name: string; + value: string; + }[]; + }; +} + +export interface IFieldText { key: string; label: string; type: string; @@ -85,6 +108,8 @@ export interface IField { variables: boolean; } +type IField = IFieldDropdown | IFieldText; + export interface IAuthenticationStepField { name: string; value: string | null; diff --git a/packages/web/src/components/FlowStep/index.tsx b/packages/web/src/components/FlowStep/index.tsx index 005e9549..7d1524bb 100644 --- a/packages/web/src/components/FlowStep/index.tsx +++ b/packages/web/src/components/FlowStep/index.tsx @@ -23,7 +23,13 @@ import AppIcon from 'components/AppIcon'; import { GET_APPS } from 'graphql/queries/get-apps'; import { GET_STEP_WITH_TEST_EXECUTIONS } from 'graphql/queries/get-step-with-test-executions'; import useFormatMessage from 'hooks/useFormatMessage'; -import { AppIconWrapper, AppIconStatusIconWrapper, Content, Header, Wrapper } from './style'; +import { + AppIconWrapper, + AppIconStatusIconWrapper, + Content, + Header, + Wrapper, +} from './style'; type FlowStepProps = { collapsed?: boolean; @@ -32,26 +38,29 @@ type FlowStepProps = { onOpen?: () => void; onClose?: () => void; onChange: (step: IStep) => void; -} +}; const validIcon = ; const errorIcon = ; -export default function FlowStep(props: FlowStepProps): React.ReactElement | null { +export default function FlowStep( + props: FlowStepProps +): React.ReactElement | null { const { collapsed, index, onChange } = props; - const contextButtonRef = React.useRef(null); + const contextButtonRef = React.useRef(null); const step: IStep = props.step; - const [anchorEl, setAnchorEl] = React.useState(null); + const [anchorEl, setAnchorEl] = React.useState( + null + ); const isTrigger = step.type === 'trigger'; const formatMessage = useFormatMessage(); const [currentSubstep, setCurrentSubstep] = React.useState(2); - const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }}); + const { data } = useQuery(GET_APPS, { + variables: { onlyWithTriggers: isTrigger }, + }); const [ getStepWithTestExecutions, - { - data: stepWithTestExecutionsData, - called: stepWithTestExecutionsCalled, - }, + { data: stepWithTestExecutionsData, called: stepWithTestExecutionsCalled }, ] = useLazyQuery(GET_STEP_WITH_TEST_EXECUTIONS, { fetchPolicy: 'network-only', }); @@ -64,17 +73,27 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul }, }); } - }, [collapsed, stepWithTestExecutionsCalled, getStepWithTestExecutions, step.id, isTrigger]); + }, [ + collapsed, + stepWithTestExecutionsCalled, + getStepWithTestExecutions, + step.id, + isTrigger, + ]); const apps: IApp[] = data?.getApps; const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey); const actionsOrTriggers = isTrigger ? app?.triggers : app?.actions; - const substeps = React.useMemo(() => actionsOrTriggers?.find(({ key }) => key === step.key)?.subSteps || [], [actionsOrTriggers, step?.key]); + const substeps = React.useMemo( + () => + actionsOrTriggers?.find(({ key }) => key === step.key)?.substeps || [], + [actionsOrTriggers, step?.key] + ); const handleChange = React.useCallback(({ step }: { step: IStep }) => { onChange(step); - }, []) + }, []); const expandNextStep = React.useCallback(() => { setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1); @@ -82,24 +101,28 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul const handleSubmit = (val: any) => { handleChange({ step: val as IStep }); - } + }; - if (!apps) return null; + if (!apps) return null; const onContextMenuClose = (event: React.SyntheticEvent) => { event.stopPropagation(); setAnchorEl(null); - } + }; const onContextMenuClick = (event: React.SyntheticEvent) => { event.stopPropagation(); setAnchorEl(contextButtonRef.current); - } + }; const onOpen = () => collapsed && props.onOpen?.(); const onClose = () => props.onClose?.(); - const toggleSubstep = (substepIndex: number) => setCurrentSubstep((value) => value !== substepIndex ? substepIndex : null); + const toggleSubstep = (substepIndex: number) => + setCurrentSubstep((value) => + value !== substepIndex ? substepIndex : null + ); - const validationStatusIcon = step.status === 'completed' ? validIcon : errorIcon; + const validationStatusIcon = + step.status === 'completed' ? validIcon : errorIcon; return ( @@ -115,11 +138,9 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
- { - isTrigger ? - formatMessage('flowStep.triggerType') : - formatMessage('flowStep.actionType') - } + {isTrigger + ? formatMessage('flowStep.triggerType') + : formatMessage('flowStep.actionType')} @@ -129,9 +150,15 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul {/* as there are no other actions besides "delete step", we hide the context menu. */} - {!isTrigger && - - } + {!isTrigger && ( + + + + )} @@ -139,7 +166,11 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul - +
- {substeps?.length > 0 && substeps.map((substep: { name: string, key: string, arguments: IField[] }, index: number) => ( - - {substep.key === 'chooseAccount' && ( - toggleSubstep((index + 1))} - onCollapse={() => toggleSubstep((index + 1))} - onSubmit={expandNextStep} - onChange={handleChange} - step={step} - /> - )} + {substeps?.length > 0 && + substeps.map( + ( + substep: { + name: string; + key: string; + arguments: IField[]; + }, + index: number + ) => ( + + {substep.key === 'chooseAccount' && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + step={step} + /> + )} - {substep.key === 'testStep' && ( - toggleSubstep((index + 1))} - onCollapse={() => toggleSubstep((index + 1))} - onSubmit={expandNextStep} - onChange={handleChange} - step={step} - /> - )} + {substep.key === 'testStep' && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + step={step} + /> + )} - {['chooseAccount', 'testStep'].includes(substep.key) === false && ( - toggleSubstep((index + 1))} - onCollapse={() => toggleSubstep((index + 1))} - onSubmit={expandNextStep} - onChange={handleChange} - step={step} - /> - )} - - ))} + {['chooseAccount', 'testStep'].includes(substep.key) === + false && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + step={step} + /> + )} + + ) + )}
@@ -200,12 +242,14 @@ export default function FlowStep(props: FlowStepProps): React.ReactElement | nul
- {anchorEl && } + {anchorEl && ( + + )} - ) -}; + ); +} diff --git a/packages/web/src/graphql/queries/get-app.ts b/packages/web/src/graphql/queries/get-app.ts index 9a8c87bd..08fb2a96 100644 --- a/packages/web/src/graphql/queries/get-app.ts +++ b/packages/web/src/graphql/queries/get-app.ts @@ -2,7 +2,7 @@ import { gql } from '@apollo/client'; export const GET_APP = gql` query GetApp($key: AvailableAppsEnumType!) { - getApp (key: $key) { + getApp(key: $key) { name key iconUrl @@ -54,7 +54,7 @@ export const GET_APP = gql` name key description - subSteps { + substeps { name } } @@ -62,7 +62,7 @@ export const GET_APP = gql` name key description - subSteps { + substeps { name } } diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts index 6e872e07..5e4d6adf 100644 --- a/packages/web/src/graphql/queries/get-apps.ts +++ b/packages/web/src/graphql/queries/get-apps.ts @@ -53,7 +53,7 @@ export const GET_APPS = gql` name key description - subSteps { + substeps { key name arguments { @@ -68,7 +68,7 @@ export const GET_APPS = gql` name key description - subSteps { + substeps { key name arguments { @@ -83,4 +83,4 @@ export const GET_APPS = gql` } } } -`; \ No newline at end of file +`;