2
0

feat(integration): Add Google Sheets integration

This commit is contained in:
Baptiste Arnaud
2022-01-18 18:25:18 +01:00
parent 2814a352b2
commit f49b5143cf
67 changed files with 2560 additions and 391 deletions

View File

@ -4,29 +4,18 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { ChatStep } from './ChatStep'
import { AvatarSideContainer } from './AvatarSideContainer'
import { HostAvatarsContext } from '../../contexts/HostAvatarsContext'
import {
ChoiceInputStep,
ComparisonOperators,
ConditionStep,
LogicalOperator,
LogicStep,
LogicStepType,
Step,
Target,
} from 'models'
import { Step, Target } from 'models'
import { useTypebot } from '../../contexts/TypebotContext'
import {
isChoiceInput,
isDefined,
isInputStep,
isIntegrationStep,
isLogicStep,
isTextBubbleStep,
} from 'utils'
import {
evaluateExpression,
isMathFormula,
parseVariables,
} from 'services/variable'
import { executeLogic } from 'services/logic'
import { getSingleChoiceTargetId } from 'services/inputs'
import { executeIntegration } from 'services/integration'
type ChatBlockProps = {
stepIds: string[]
@ -50,12 +39,29 @@ export const ChatBlock = ({
useEffect(() => {
autoScrollToBottom()
onNewStepDisplayed()
}, [displayedSteps])
const onNewStepDisplayed = async () => {
const currentStep = [...displayedSteps].pop()
if (currentStep && isLogicStep(currentStep)) {
const target = executeLogic(currentStep)
if (!currentStep) return
if (isLogicStep(currentStep)) {
const target = executeLogic(
currentStep,
typebot.variables,
updateVariableValue
)
target ? onBlockEnd(target) : displayNextStep()
}
}, [displayedSteps])
if (isIntegrationStep(currentStep)) {
const target = await executeIntegration(
currentStep,
typebot.variables,
updateVariableValue
)
target ? onBlockEnd(target) : displayNextStep()
}
}
const autoScrollToBottom = () => {
scroll.scrollToBottom({
@ -77,7 +83,13 @@ export const ChatBlock = ({
const isSingleChoiceStep =
isChoiceInput(currentStep) && !currentStep.options.isMultipleChoice
if (isSingleChoiceStep)
return onBlockEnd(getSingleChoiceTargetId(currentStep, answerContent))
return onBlockEnd(
getSingleChoiceTargetId(
currentStep,
typebot.choiceItems,
answerContent
)
)
if (
currentStep?.target?.blockId ||
displayedSteps.length === stepIds.length
@ -88,67 +100,6 @@ export const ChatBlock = ({
if (nextStep) setDisplayedSteps([...displayedSteps, nextStep])
}
const executeLogic = (step: LogicStep): Target | undefined => {
switch (step.type) {
case LogicStepType.SET_VARIABLE: {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return
const expression = step.options.expressionToEvaluate
const evaluatedExpression = isMathFormula(expression)
? evaluateExpression(parseVariables(expression, typebot.variables))
: expression
updateVariableValue(step.options.variableId, evaluatedExpression)
return
}
case LogicStepType.CONDITION: {
const isConditionPassed =
step.options?.logicalOperator === LogicalOperator.AND
? step.options?.comparisons.allIds.every(executeComparison(step))
: step.options?.comparisons.allIds.some(executeComparison(step))
return isConditionPassed ? step.trueTarget : step.falseTarget
}
}
}
const executeComparison = (step: ConditionStep) => (comparisonId: string) => {
const comparison = step.options?.comparisons.byId[comparisonId]
if (!comparison?.variableId) return false
const inputValue = typebot.variables.byId[comparison.variableId].value ?? ''
const { value } = comparison
if (!isDefined(value)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
return inputValue.includes(value)
}
case ComparisonOperators.EQUAL: {
return inputValue === value
}
case ComparisonOperators.NOT_EQUAL: {
return inputValue !== value
}
case ComparisonOperators.GREATER: {
return parseFloat(inputValue) >= parseFloat(value)
}
case ComparisonOperators.LESS: {
return parseFloat(inputValue) <= parseFloat(value)
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
}
}
const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep,
answerContent?: string
): Target | undefined => {
const itemId = currentStep.options.itemIds.find(
(itemId) => typebot.choiceItems.byId[itemId].content === answerContent
)
if (!itemId) throw new Error('itemId should exist')
return typebot.choiceItems.byId[itemId].target ?? currentStep.target
}
return (
<div className="flex">
<HostAvatarsContext>

View File

@ -0,0 +1,13 @@
import { ChoiceInputStep, ChoiceItem, Table, Target } from 'models'
export const getSingleChoiceTargetId = (
currentStep: ChoiceInputStep,
choiceItems: Table<ChoiceItem>,
answerContent?: string
): Target | undefined => {
const itemId = currentStep.options.itemIds.find(
(itemId) => choiceItems.byId[itemId].content === answerContent
)
if (!itemId) throw new Error('itemId should exist')
return choiceItems.byId[itemId].target ?? currentStep.target
}

View File

@ -0,0 +1,121 @@
import {
IntegrationStep,
IntegrationStepType,
GoogleSheetsStep,
GoogleSheetsAction,
GoogleSheetsInsertRowOptions,
Variable,
Table,
GoogleSheetsUpdateRowOptions,
Cell,
GoogleSheetsGetOptions,
} from 'models'
import { stringify } from 'qs'
import { sendRequest } from 'utils'
import { parseVariables } from './variable'
export const executeIntegration = (
step: IntegrationStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
switch (step.type) {
case IntegrationStepType.GOOGLE_SHEETS:
return executeGoogleSheetIntegration(step, variables, updateVariableValue)
}
}
const executeGoogleSheetIntegration = async (
step: GoogleSheetsStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
if (!step.options) return step.target
switch (step.options?.action) {
case GoogleSheetsAction.INSERT_ROW:
await insertRowInGoogleSheets(step.options, variables)
break
case GoogleSheetsAction.UPDATE_ROW:
await updateRowInGoogleSheets(step.options, variables)
break
case GoogleSheetsAction.GET:
await getRowFromGoogleSheets(step.options, variables, updateVariableValue)
break
}
return step.target
}
const insertRowInGoogleSheets = async (
options: GoogleSheetsInsertRowOptions,
variables: Table<Variable>
) => {
if (!options.cellsToInsert) return
return sendRequest({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'POST',
body: {
credentialsId: options.credentialsId,
values: parseCellValues(options.cellsToInsert, variables),
},
})
}
const updateRowInGoogleSheets = async (
options: GoogleSheetsUpdateRowOptions,
variables: Table<Variable>
) => {
if (!options.cellsToUpsert || !options.referenceCell) return
return sendRequest({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}`,
method: 'PATCH',
body: {
credentialsId: options.credentialsId,
values: parseCellValues(options.cellsToUpsert, variables),
referenceCell: {
column: options.referenceCell.column,
value: parseVariables(options.referenceCell.value ?? '', variables),
},
},
})
}
const getRowFromGoogleSheets = async (
options: GoogleSheetsGetOptions,
variables: Table<Variable>,
updateVariableValue: (variableId: string, value: string) => void
) => {
if (!options.referenceCell || !options.cellsToExtract) return
const queryParams = stringify(
{
credentialsId: options.credentialsId,
referenceCell: {
column: options.referenceCell.column,
value: parseVariables(options.referenceCell.value ?? '', variables),
},
columns: options.cellsToExtract.allIds.map(
(id) => options.cellsToExtract?.byId[id].column
),
},
{ indices: false }
)
const { data } = await sendRequest<{ [key: string]: string }>({
url: `http://localhost:3001/api/integrations/google-sheets/spreadsheets/${options.spreadsheetId}/sheets/${options.sheetId}?${queryParams}`,
method: 'GET',
})
if (!data) return
options.cellsToExtract.allIds.forEach((cellId) => {
const cell = options.cellsToExtract?.byId[cellId]
if (!cell) return
updateVariableValue(cell.variableId ?? '', data[cell.column ?? ''])
})
}
const parseCellValues = (
cells: Table<Cell>,
variables: Table<Variable>
): { [key: string]: string } =>
cells.allIds.reduce((row, id) => {
const cell = cells.byId[id]
return !cell.column || !cell.value
? row
: { ...row, [cell.column]: parseVariables(cell.value, variables) }
}, {})

View File

@ -0,0 +1,72 @@
import {
LogicStep,
Target,
LogicStepType,
LogicalOperator,
ConditionStep,
Table,
Variable,
ComparisonOperators,
} from 'models'
import { isDefined } from 'utils'
import { isMathFormula, evaluateExpression, parseVariables } from './variable'
export const executeLogic = (
step: LogicStep,
variables: Table<Variable>,
updateVariableValue: (variableId: string, expression: string) => void
): Target | undefined => {
switch (step.type) {
case LogicStepType.SET_VARIABLE: {
if (!step.options?.variableId || !step.options.expressionToEvaluate)
return
const expression = step.options.expressionToEvaluate
const evaluatedExpression = isMathFormula(expression)
? evaluateExpression(parseVariables(expression, variables))
: expression
updateVariableValue(step.options.variableId, evaluatedExpression)
return
}
case LogicStepType.CONDITION: {
const isConditionPassed =
step.options?.logicalOperator === LogicalOperator.AND
? step.options?.comparisons.allIds.every(
executeComparison(step, variables)
)
: step.options?.comparisons.allIds.some(
executeComparison(step, variables)
)
return isConditionPassed ? step.trueTarget : step.falseTarget
}
}
}
const executeComparison =
(step: ConditionStep, variables: Table<Variable>) =>
(comparisonId: string) => {
const comparison = step.options?.comparisons.byId[comparisonId]
if (!comparison?.variableId) return false
const inputValue = variables.byId[comparison.variableId].value ?? ''
const { value } = comparison
if (!isDefined(value)) return false
switch (comparison.comparisonOperator) {
case ComparisonOperators.CONTAINS: {
return inputValue.includes(value)
}
case ComparisonOperators.EQUAL: {
return inputValue === value
}
case ComparisonOperators.NOT_EQUAL: {
return inputValue !== value
}
case ComparisonOperators.GREATER: {
return parseFloat(inputValue) >= parseFloat(value)
}
case ComparisonOperators.LESS: {
return parseFloat(inputValue) <= parseFloat(value)
}
case ComparisonOperators.IS_SET: {
return isDefined(inputValue) && inputValue.length > 0
}
}
}