diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx index f5630532c..ce6ab0deb 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx +++ b/apps/builder/src/features/blocks/integrations/googleSheets/components/GoogleSheetsSettings.tsx @@ -214,21 +214,61 @@ const ActionOptions = ({ ) case GoogleSheetsAction.UPDATE_ROW: return ( - - Row to select - - Cells to update - - initialItems={options.cellsToUpsert} - onItemsChange={handleUpsertColumnsChange} - Item={UpdatingCellItem} - addLabel="Add a value" - /> - + + {options.referenceCell && ( + + + + Row to update + + + + + + + + + )} + {!options.referenceCell && ( + + + + Row(s) to update + + + + + + + + + )} + + + + Cells to update + + + + + + + initialItems={options.cellsToUpsert} + onItemsChange={handleUpsertColumnsChange} + Item={UpdatingCellItem} + addLabel="Add a value" + /> + + + ) case GoogleSheetsAction.GET: return ( diff --git a/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts b/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts index 3c4660fec..2673e3abc 100644 --- a/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts +++ b/apps/builder/src/features/blocks/integrations/googleSheets/googleSheets.spec.ts @@ -58,12 +58,16 @@ test.describe.parallel('Google sheets integration', () => { await page.click('text=Select an operation') await page.click('text=Update a row') - await page.click('text=Add a value') + await page.getByRole('button', { name: 'Row(s) to update' }).click() + await page.getByRole('button', { name: 'Add filter rule' }).click() await page.click('text=Select a column') await page.click('button >> text="Email"') + await page.getByRole('button', { name: 'Select an operator' }).click() + await page.getByRole('menuitem', { name: 'Equal to' }).click() await page.click('[aria-label="Insert a variable"]') await page.click('button >> text="Email" >> nth=1') + await page.getByRole('button', { name: 'Cells to update' }).click() await page.click('text=Add a value') await page.click('text=Select a column') await page.click('text=Last name') @@ -82,7 +86,7 @@ test.describe.parallel('Google sheets integration', () => { .locator('input[placeholder="Type your email..."]') .press('Enter') await expect( - page.getByText('Succesfully updated row in CRM > Sheet1').nth(0) + page.getByText('Succesfully updated matching rows').nth(0) ).toBeVisible() }) diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts index 63d7fde9e..06d5804fb 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/getRow.ts @@ -2,17 +2,15 @@ import { SessionState, GoogleSheetsGetOptions, VariableWithValue, - ComparisonOperators, - LogicalOperator, ReplyLog, } from '@typebot.io/schemas' -import { isNotEmpty, byId, isDefined } from '@typebot.io/lib' -import type { GoogleSpreadsheetRow } from 'google-spreadsheet' +import { isNotEmpty, byId } from '@typebot.io/lib' import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc' import { ExecuteIntegrationResponse } from '@/features/chat/types' import { saveErrorLog } from '@/features/logs/saveErrorLog' import { updateVariables } from '@/features/variables/updateVariables' import { deepParseVariables } from '@/features/variables/deepParseVariable' +import { matchFilter } from './helpers/matchFilter' export const getRow = async ( state: SessionState, @@ -101,56 +99,3 @@ export const getRow = async ( } return { outgoingEdgeId, logs: log ? [log] : undefined } } - -const matchFilter = ( - row: GoogleSpreadsheetRow, - filter: NonNullable -) => { - return filter.logicalOperator === LogicalOperator.AND - ? filter.comparisons.every( - (comparison) => - comparison.column && - matchComparison( - row[comparison.column], - comparison.comparisonOperator, - comparison.value - ) - ) - : filter.comparisons.some( - (comparison) => - comparison.column && - matchComparison( - row[comparison.column], - comparison.comparisonOperator, - comparison.value - ) - ) -} - -const matchComparison = ( - inputValue?: string, - comparisonOperator?: ComparisonOperators, - value?: string -) => { - if (!inputValue || !comparisonOperator || !value) return false - switch (comparisonOperator) { - case ComparisonOperators.CONTAINS: { - return inputValue.toLowerCase().includes(value.toLowerCase()) - } - 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 - } - } -} diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/matchFilter.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/matchFilter.ts new file mode 100644 index 000000000..119feb966 --- /dev/null +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/helpers/matchFilter.ts @@ -0,0 +1,72 @@ +import { isDefined } from '@typebot.io/lib' +import { + GoogleSheetsGetOptions, + LogicalOperator, + ComparisonOperators, +} from '@typebot.io/schemas' +import { GoogleSpreadsheetRow } from 'google-spreadsheet' + +export const matchFilter = ( + row: GoogleSpreadsheetRow, + filter: NonNullable +) => { + return filter.logicalOperator === LogicalOperator.AND + ? filter.comparisons.every( + (comparison) => + comparison.column && + matchComparison( + row[comparison.column], + comparison.comparisonOperator, + comparison.value + ) + ) + : filter.comparisons.some( + (comparison) => + comparison.column && + matchComparison( + row[comparison.column], + comparison.comparisonOperator, + comparison.value + ) + ) +} + +const matchComparison = ( + inputValue?: string, + comparisonOperator?: ComparisonOperators, + value?: string +): boolean => { + if (!inputValue || !comparisonOperator || !value) return false + switch (comparisonOperator) { + case ComparisonOperators.CONTAINS: { + return inputValue.toLowerCase().includes(value.toLowerCase()) + } + 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 + } + case ComparisonOperators.IS_EMPTY: { + return !isDefined(inputValue) || inputValue.length === 0 + } + case ComparisonOperators.STARTS_WITH: { + return inputValue.toLowerCase().startsWith(value.toLowerCase()) + } + case ComparisonOperators.ENDS_WITH: { + return inputValue.toLowerCase().endsWith(value.toLowerCase()) + } + case ComparisonOperators.NOT_CONTAINS: { + return !inputValue.toLowerCase().includes(value.toLowerCase()) + } + } +} diff --git a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts index 5704a7c98..17b9660d8 100644 --- a/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts +++ b/apps/viewer/src/features/blocks/integrations/googleSheets/updateRow.ts @@ -10,6 +10,7 @@ import { ExecuteIntegrationResponse } from '@/features/chat/types' import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { TRPCError } from '@trpc/server' +import { matchFilter } from './helpers/matchFilter' export const updateRow = async ( { result, typebot: { variables } }: SessionState, @@ -18,8 +19,9 @@ export const updateRow = async ( options, }: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions } ): Promise => { - const { sheetId, referenceCell } = deepParseVariables(variables)(options) - if (!options.cellsToUpsert || !sheetId || !referenceCell) + const { sheetId, referenceCell, filter } = + deepParseVariables(variables)(options) + if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter)) return { outgoingEdgeId } let log: ReplyLog | undefined @@ -34,14 +36,16 @@ export const updateRow = async ( await doc.loadInfo() const sheet = doc.sheetsById[sheetId] const rows = await sheet.getRows() - const updatingRowIndex = rows.findIndex( - (row) => row[referenceCell.column as string] === referenceCell.value + const filteredRows = rows.filter((row) => + referenceCell + ? row[referenceCell.column as string] === referenceCell.value + : matchFilter(row, filter as NonNullable) ) - if (updatingRowIndex === -1) { + if (filteredRows.length === 0) { log = { status: 'error', - description: `Could not find row to update`, - details: `Looked for row with ${referenceCell.column} equals to "${referenceCell.value}"`, + description: `Could not find any row that matches the filter`, + details: JSON.stringify(filter, null, 2), } result && (await saveErrorLog({ @@ -51,19 +55,22 @@ export const updateRow = async ( })) throw new TRPCError({ code: 'NOT_FOUND', - message: `Couldn't find row with ${referenceCell.column} that equals to "${referenceCell.value}"`, + message: `Could not find any row that matches the filter`, }) } - for (const key in parsedValues) { - rows[updatingRowIndex][key] = parsedValues[key] - } - try { - await rows[updatingRowIndex].save() + for (const filteredRow of filteredRows) { + const rowIndex = rows.findIndex((row) => row.id === filteredRow.id) + for (const key in parsedValues) { + rows[rowIndex][key] = parsedValues[key] + } + await rows[rowIndex].save() + } + log = log = { status: 'success', - description: `Succesfully updated row in ${doc.title} > ${sheet.title}`, + description: `Succesfully updated matching rows`, } result && (await saveSuccessLog({ diff --git a/packages/schemas/features/blocks/integrations/googleSheets/schemas.ts b/packages/schemas/features/blocks/integrations/googleSheets/schemas.ts index aad3618ca..d0624f259 100644 --- a/packages/schemas/features/blocks/integrations/googleSheets/schemas.ts +++ b/packages/schemas/features/blocks/integrations/googleSheets/schemas.ts @@ -38,8 +38,9 @@ const initialGoogleSheetsOptionsSchema = googleSheetsOptionsBaseSchema.merge( const googleSheetsGetOptionsSchema = googleSheetsOptionsBaseSchema.merge( z.object({ action: z.enum([GoogleSheetsAction.GET]), - // TODO: remove referenceCell once migrated to filtering - referenceCell: cellSchema.optional(), + referenceCell: cellSchema + .optional() + .describe('Deprecated. Use `filter` instead.'), filter: z .object({ comparisons: z.array(rowsFilterComparisonSchema), @@ -61,7 +62,15 @@ const googleSheetsUpdateRowOptionsSchema = googleSheetsOptionsBaseSchema.merge( z.object({ action: z.enum([GoogleSheetsAction.UPDATE_ROW]), cellsToUpsert: z.array(cellSchema), - referenceCell: cellSchema.optional(), + referenceCell: cellSchema + .optional() + .describe('Deprecated. Use `filter` instead.'), + filter: z + .object({ + comparisons: z.array(rowsFilterComparisonSchema), + logicalOperator: z.nativeEnum(LogicalOperator), + }) + .optional(), }) )