Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,45 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpRequest,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const addWorksheetAction = createAction({
auth: excelAuth,
name: 'add_worksheet',
description: 'Add a worksheet to a workbook',
displayName: 'Add a Worksheet to a Workbook',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_name: Property.ShortText({
displayName: 'Worksheet Name',
description: 'The name of the new worksheet',
required: false,
defaultValue: 'Sheet',
}),
},
async run({ propsValue, auth }) {
const workbook_id = propsValue['workbook_id'];
const worksheet_name = propsValue['worksheet_name'];
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets`,
body: {
name: worksheet_name,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
});

View File

@@ -0,0 +1,66 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpRequest,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon, objectToArray } from '../common/common';
export const appendRowAction = createAction({
auth: excelAuth,
name: 'append_row',
description: 'Append row of values to a worksheet',
displayName: 'Append Row to Worksheet',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
first_row_headers: Property.Checkbox({
displayName: 'Does the first row contain headers?',
description: 'If the first row is headers',
required: true,
defaultValue: false,
}),
values: excelCommon.values,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const values = propsValue.first_row_headers
? objectToArray(propsValue['values'])
: Object.values(propsValue['values'])[0];
const lastUsedRow = await excelCommon.getLastUsedRow(
workbookId,
worksheetId,
auth['access_token'],
);
const lastUsedColumn = excelCommon.numberToColumnName(Object.values(values).length);
const rangeFrom = `A${lastUsedRow + 1}`;
const rangeTo = `${lastUsedColumn}${lastUsedRow + 1}`;
const url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${rangeFrom}:${rangeTo}')`;
const requestBody = {
values: [values],
};
const request: HttpRequest = {
method: HttpMethod.PATCH,
url: url,
body: requestBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
headers: {
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
});

View File

@@ -0,0 +1,41 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../..';
import { excelCommon } from '../common/common';
export const appendTableRowsAction = createAction({
auth: excelAuth,
name: 'append_table_rows',
description: 'Append rows to a table',
displayName: 'Append Rows to a Table',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
values: excelCommon.table_values,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableId = propsValue['table_id'];
const valuesToAppend = [Object.values(propsValue['values'])];
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`,
body: {
values: valuesToAppend,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
return response.body;
},
});

View File

@@ -0,0 +1,70 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const clearRangeAction = createAction({
auth: excelAuth,
name: 'clear_range',
displayName: 'Clear Cells by Range',
description: 'Clear a block of cells (range) content or formatting.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
range: Property.ShortText({
displayName: 'Range',
description:
'The range of cells to clear, in A1 notation (e.g., "A1:C5").',
required: true
}),
applyTo: Property.StaticDropdown({
displayName: 'Clear Type',
description: 'Specify what to clear from the range.',
required: true,
defaultValue: 'All',
options: {
options: [
{
label: 'All (Contents and Formatting)',
value: 'All'
},
{
label: 'Contents Only',
value: 'Contents'
},
{
label: 'Formats Only',
value: 'Formats'
}
]
}
})
},
async run(context) {
const { workbook_id, worksheet_id, range, applyTo } = context.propsValue;
const { access_token } = context.auth;
if (!/^[A-Z]+[1-9][0-9]*(:[A-Z]+[1-9][0-9]*)?$/.test(range as string)) {
throw new Error('Invalid range format. Please use A1 notation (e.g., "A1" or "A1:C5").');
}
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}/range(address='${range}')/clear`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
applyTo: applyTo
}
});
// A successful request returns a 200 OK with no body.
return response.body;
}
});

View File

@@ -0,0 +1,81 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const clearColumnAction = createAction({
auth: excelAuth,
name: 'clear_column',
displayName: 'Clear Column by Index',
description: 'Clear contents/formatting of a column by its index.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
column_index: Property.Number({
displayName: 'Column Index',
description:
'The 1-based index of the column to be cleared (e.g., 1 for column A, 2 for column B).',
required: true
}),
applyTo: Property.StaticDropdown({
displayName: 'Clear Type',
description: 'Specify what to clear from the column.',
required: true,
defaultValue: 'All',
options: {
options: [
{
label: 'All (Contents and Formatting)',
value: 'All'
},
{
label: 'Contents Only',
value: 'Contents'
},
{
label: 'Formats Only',
value: 'Formats'
}
]
}
})
},
async run(context) {
const { workbook_id, worksheet_id, column_index, applyTo } =
context.propsValue;
const { access_token } = context.auth;
if (
typeof column_index !== 'number' ||
!Number.isInteger(column_index) ||
column_index < 1
) {
throw new Error('Column index must be a positive integer.');
}
// Convert 1-based index to Excel column letter (e.g., 1 -> 'A')
const columnLetter = excelCommon.numberToColumnName(column_index);
// Construct the range address for the entire column, e.g., 'C:C'
const columnAddress = `${columnLetter}:${columnLetter}`;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}/range(address='${columnAddress}')/clear`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
applyTo: applyTo
}
});
// A successful request returns a 200 OK with no body.
return response.body;
}
});

View File

@@ -0,0 +1,69 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const clearRowAction = createAction({
auth: excelAuth,
name: 'clear_row',
displayName: 'Clear Row by ID',
description: 'Clear contents/formatting of an entire row by its ID.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
row_id: Property.Number({
displayName: 'Row Number',
description: 'The number of the row to be cleared (e.g., 5 for the 5th row).',
required: true,
}),
applyTo: Property.StaticDropdown({
displayName: "Clear Type",
description: "Specify what to clear from the row.",
required: true,
defaultValue: 'All',
options: {
options: [
{
label: 'All (Contents and Formatting)',
value: 'All'
},
{
label: 'Contents Only',
value: 'Contents'
},
{
label: 'Formats Only',
value: 'Formats'
}
]
}
})
},
async run(context) {
const { workbook_id, worksheet_id, row_id, applyTo } = context.propsValue;
const { access_token } = context.auth;
if (typeof row_id !== 'number' || !Number.isInteger(row_id) || row_id < 1) {
throw new Error('Row index must be a positive integer.');
}
// Construct the range address for the entire row, e.g., '5:5'
const rowAddress = `${row_id}:${row_id}`;
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}/range(address='${rowAddress}')/clear`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token,
},
body: {
applyTo: applyTo,
},
});
// A successful request returns a 200 OK with no body.
return response.body;
},
});

View File

@@ -0,0 +1,54 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const clearWorksheetAction = createAction({
auth: excelAuth,
name: 'clear_worksheet',
description: 'Clear a worksheet',
displayName: 'Clear Worksheet',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
range: Property.ShortText({
displayName: 'Range',
description:
'The range in A1 notation (e.g., A2:B2) to clear in the worksheet, if not provided, clear the entire worksheet',
required: false,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const range = propsValue['range'];
let url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/`;
// If range is not provided, clear the entire worksheet
if (!range) {
url += 'usedRange(valuesOnly=true)/clear';
} else {
url += `range(address = '${range}')/clear`;
}
const request = {
method: HttpMethod.POST,
url: url,
body: {
applyTo: 'contents',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN as const,
token: auth['access_token'],
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelCommon } from '../common/common';
import { excelAuth } from '../../index';
export const convertToRangeAction = createAction({
auth: excelAuth,
name: 'convert_to_range',
description: 'Converts a table to a range',
displayName: 'Convert to Range',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableId = propsValue['table_id'];
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/convertToRange`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
return response.body;
},
});

View File

@@ -0,0 +1,94 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const createTableAction = createAction({
auth: excelAuth,
name: 'create_table',
description: 'Create a table in a worksheet',
displayName: 'Create Table',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
selectRange: Property.Dropdown({
auth: excelAuth,
displayName: 'Select Range',
description: 'How to select the range for the table',
required: true,
options: async () => {
return {
disabled: false,
options: [
{
label: 'Automatically',
value: 'auto',
},
{
label: 'Manually',
value: 'manual',
},
],
defaultValue: 'auto',
};
},
refreshers: [],
}),
range: Property.ShortText({
displayName: 'Range',
description:
'The range of cells in A1 notation (e.g., A2:B2) that will be converted to a table',
required: false,
defaultValue: 'A1:B2',
}),
hasHeaders: Property.Checkbox({
displayName: 'Has Headers',
description: 'Whether the range has column labels',
required: true,
defaultValue: true,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const selectRange = propsValue['selectRange'];
const hasHeaders = propsValue['hasHeaders'];
let range: string | undefined;
if (selectRange === 'auto') {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
queryParams: {
select: 'address',
},
});
range = response.body['address'].split('!')[1];
} else {
range = propsValue['range'];
}
const result = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/add`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
body: {
address: range,
hasHeaders,
},
});
return result.body;
},
});

View File

@@ -0,0 +1,45 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
export const createWorkbook = createAction({
auth: excelAuth,
name: 'createWorkbook',
displayName: 'Create Workbook',
description: 'Create a new workbook at the specified location',
props: {
name: Property.ShortText({
displayName: "Name",
description: "The name of the new workbook",
required: true
}),
parentFolder: excelCommon.parent_folder
},
async run(context) {
const { name, parentFolder } = context.propsValue
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/${parentFolder === 'root' ? '' : 'items/'}${parentFolder}/children`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth['access_token'],
},
headers: {
'Content-Type': 'application/json'
},
body: {
file: {},
name: name.endsWith('.xlsx') ? name : `${name}.xlsx`,
'@microsoft.graph.conflictBehavior': 'rename'
}
})
return response
}
});

View File

@@ -0,0 +1,102 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
// Define the response type for creating a worksheet for better type-safety
interface CreateWorksheetResponse {
id: string;
name: string;
position: number;
visibility: string;
}
export const createWorksheetAction = createAction({
auth: excelAuth,
name: 'create_worksheet',
displayName: 'Create Worksheet',
description:
'Add a new worksheet (tab) to an existing workbook with optional default headers.',
props: {
workbook_id: excelCommon.workbook_id,
name: Property.ShortText({
displayName: 'Worksheet Name',
description:
"The name for the new worksheet. If not provided, a default name like 'Sheet1' will be assigned.",
required: false
}),
headers: Property.Array({
displayName: 'Headers',
description:
'Optional: A list of headers to add to the first row. A table will be created from these headers.',
required: false
})
},
async run(context) {
const { workbook_id, name, headers } = context.propsValue;
const { access_token } = context.auth;
// Step 1: Create the new worksheet
const createWorksheetResponse =
await httpClient.sendRequest<CreateWorksheetResponse>({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/add`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
// Conditionally add the name property to the request body if it exists
...(name ? { name } : {})
}
});
const newWorksheetName = createWorksheetResponse.body.name;
// Step 2: If headers are provided, add them and create a table from them
if (headers && Array.isArray(headers) && headers.length > 0) {
const headersArray = headers as string[];
// Calculate the table range, e.g., "A1:C1" for 3 headers
const endColumn = excelCommon.numberToColumnName(headersArray.length);
const address = `A1:${endColumn}1`;
// Add the header values to the first row of the new worksheet
await httpClient.sendRequest({
method: HttpMethod.PATCH,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${newWorksheetName}/range(address='${address}')`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
values: [headersArray] // Values must be a 2D array
}
});
// Create a table from the newly added header range
const createTableResponse = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${newWorksheetName}/tables/add`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
address: address,
hasHeaders: true
}
});
// Return the result of the table creation
return createTableResponse.body;
}
// If no headers were provided, return the result of the worksheet creation
return createWorksheetResponse.body;
}
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const deleteTableAction = createAction({
auth: excelAuth,
name: 'delete_table',
description: 'Delete a table from a worksheet',
displayName: 'Delete Table',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableId = propsValue['table_id'];
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
return response.body;
},
});

View File

@@ -0,0 +1,35 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpRequest,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const deleteWorkbookAction = createAction({
auth: excelAuth,
name: 'delete_workbook',
description: 'Delete a workbook',
displayName: 'Delete Workbook',
props: {
workbook_id: excelCommon.workbook_id,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const accessToken = auth['access_token'];
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${excelCommon.baseUrl}/items/${workbookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
await httpClient.sendRequest(request);
return { success: true };
},
});

View File

@@ -0,0 +1,35 @@
import { createAction } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const deleteWorksheetAction = createAction({
auth: excelAuth,
name: 'delete_worksheet',
description: 'Delete a worksheet in a workbook',
displayName: 'Delete Worksheet',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const request = {
method: HttpMethod.DELETE,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN as const,
token: auth['access_token'],
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
});

View File

@@ -0,0 +1,121 @@
import {
createAction,
Property,
OAuth2PropertyValue
} from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const findRowAction = createAction({
auth: excelAuth,
name: 'find_row',
displayName: 'Find Row',
description:
'Locate a row by specifying a lookup column and value (e.g. find a row where “ID” = 123).',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
lookup_column: Property.Dropdown({
auth: excelAuth,
displayName: 'Lookup Column',
description: 'The column to search in.',
required: true,
refreshers: ['workbook_id', 'table_id'],
options: async ({ auth, workbook_id, table_id }) => {
if (!auth || !workbook_id || !table_id) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workbook and table first.'
};
}
const authProp = auth as OAuth2PropertyValue;
const response = await httpClient.sendRequest<{
value: { id: string; name: string }[];
}>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/tables/${table_id}/columns`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token
}
});
return {
disabled: false,
options: response.body.value.map((column) => ({
label: column.name,
value: column.id
}))
};
}
}),
lookup_value: Property.ShortText({
displayName: 'Lookup Value',
description: 'The value to find in the lookup column.',
required: true
})
},
async run(context) {
const { workbook_id, table_id, lookup_column, lookup_value } =
context.propsValue;
const { access_token } = context.auth;
const columnId = lookup_column;
const sanitizedValue = (lookup_value as string).replace(/'/g, "''");
// Define the URL to clear the filter, which will be used in the 'finally' block
const clearFilterUrl = `${excelCommon.baseUrl}/items/${workbook_id}/workbook/tables/${table_id}/columns/${columnId}/filter/clear`;
try {
// Step 1: Apply the filter to the specified column
await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/tables/${table_id}/columns/${columnId}/filter/apply`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
criteria: {
criterion1: `=${sanitizedValue}`,
filterOn: 'Custom'
}
}
});
// Step 2: Get the visible rows (i.e., the filtered results)
const foundRowsResponse = await httpClient.sendRequest<{
value: unknown[];
}>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/tables/${table_id}/range/visibleView/rows`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
}
});
// The result is the array of rows that matched the filter
return foundRowsResponse.body.value;
} finally {
// Step 3: Clear the filter to restore the table to its original state.
// This runs regardless of whether the previous steps succeeded or failed.
await httpClient.sendRequest({
method: HttpMethod.POST,
url: clearFilterUrl,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {} // Clear action does not require a body
});
}
}
});

View File

@@ -0,0 +1,48 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const getRangeAction = createAction({
auth: excelAuth,
name: 'get_range',
displayName: 'Get Cells in Range',
description: 'Retrieve the values in a given cell range (e.g., “A1:C10”).',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
range: Property.ShortText({
displayName: 'Range',
description:
'The range of cells to retrieve, in A1 notation (e.g., "A1:C10").',
required: true
})
},
async run(context) {
const { workbook_id, worksheet_id, range } = context.propsValue;
const { access_token } = context.auth;
if (!/^[A-Z]+[1-9][0-9]*(:[A-Z]+[1-9][0-9]*)?$/.test(range as string)) {
throw new Error(
'Invalid range format. Please use A1 notation (e.g., "A1" or "A1:C5").'
);
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}/range(address='${range}')`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
}
});
// The response body contains the workbookRange object with details
// like values, text, formulas, rowCount, etc.
return response.body;
}
});

View File

@@ -0,0 +1,67 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpError
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export const getRowAction = createAction({
auth: excelAuth,
name: 'getRowById',
displayName: 'Get Row by ID',
description: '  Retrieve the entire content of a row by its row ID.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
row_id: Property.Number({
displayName: 'Row ID (Index)',
description:
'The zero-based index of the row to retrieve (e.g., 0 for the first row, 1 for the second).',
required: true
})
},
async run(context) {
const { workbook_id, table_id, row_id } = context.propsValue;
const { access_token } = context.auth;
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/tables/${table_id}/rows/itemAt(index=${row_id})`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
}
});
return response.body;
} catch (error) {
const httpError = error as HttpError;
if (httpError.response?.status === 503 && attempt < maxRetries - 1) {
const delayMs = 2 ** attempt * 1000;
console.warn(
`Excel API is unavailable (503). Retrying after ${delayMs}ms... (Attempt ${
attempt + 1
}/${maxRetries})`
);
await delay(delayMs);
attempt++;
} else {
throw error;
}
}
}
throw new Error(
'Failed to retrieve row after multiple retries due to API unavailability.'
);
}
});

View File

@@ -0,0 +1,52 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import { excelAuth } from '../..';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
export const getTableColumnsAction = createAction({
auth: excelAuth,
name: 'get_table_columns',
description: 'List columns of a table in a worksheet',
displayName: 'Get Table Columns',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table: excelCommon.table_id,
limit: Property.Number({
displayName: 'Limit',
description: 'Limit the number of columns retrieved',
required: false,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableId = propsValue['table'];
const limit = propsValue['limit'];
let url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`;
if (limit) {
url += `?$top=${limit}`;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
const columnNames = response.body['value'].map(
(column: { name: any }) => column.name
);
return columnNames;
},
});

View File

@@ -0,0 +1,52 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import { excelAuth } from '../..';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
export const getTableRowsAction = createAction({
auth: excelAuth,
name: 'get_table_rows',
description: 'List rows of a table in a worksheet',
displayName: 'Get Table Rows',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table: excelCommon.table_id,
limit: Property.Number({
displayName: 'Limit',
description: 'Limit the number of rows retrieved',
required: false,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableId = propsValue['table'];
const limit = propsValue['limit'];
let url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/rows`;
if (limit) {
url += `?$top=${limit}`;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
const rowsValues = response.body['value'].map(
(row: { values: any[] }) => row.values[0]
);
return rowsValues;
},
});

View File

@@ -0,0 +1,56 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const getWorkbooksAction = createAction({
auth: excelAuth,
name: 'get_workbooks',
description: 'Retrieve a list of workbooks',
displayName: 'Get Workbooks',
props: {
limit: Property.Number({
displayName: 'Limit',
description:
'Limits the number of workbooks returned, returns all workbooks if empty',
required: false,
}),
},
async run({ propsValue, auth }) {
const limit = propsValue['limit'];
const queryParams: any = {
$filter:
"file ne null and file/mimeType eq 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'",
};
if (limit !== null && limit !== undefined) {
queryParams.$top = limit.toString();
}
const request = {
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/root/search(q='.xlsx')`,
authentication: {
type: AuthenticationType.BEARER_TOKEN as const,
token: auth['access_token'],
},
queryParams: queryParams,
};
const response = await httpClient.sendRequest(request);
const workbooks = response.body['value'].map(
(item: { id: any; name: any; webUrl: any }) => ({
id: item.id,
name: item.name,
webUrl: item.webUrl,
})
);
return workbooks;
},
});

View File

@@ -0,0 +1,33 @@
import { createAction } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const getWorksheetAction = createAction({
auth: excelAuth,
name: 'get_worksheet',
displayName: 'Get Worksheet by ID',
description: 'Retrieve metadata of a worksheet by its ID.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id, // This dropdown provides the worksheet name as its value
},
async run(context) {
const { workbook_id, worksheet_id } = context.propsValue;
const { access_token } = context.auth;
// The worksheet_id prop from excelCommon returns the worksheet's name,
// which can be used to identify it in the API URL as per the documentation ({id|name}).
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token,
},
});
// The response body contains the workbookWorksheet object with its metadata.
return response.body;
},
});

View File

@@ -0,0 +1,73 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelCommon } from '../common/common';
import { excelAuth } from '../../index';
export const getWorksheetRowsAction = createAction({
auth: excelAuth,
name: 'get_worksheet_rows',
description: 'Retrieve rows from a worksheet',
displayName: 'Get Worksheet Rows',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
range: Property.ShortText({
displayName: 'Range',
description: 'Range of the rows to retrieve (e.g., A2:B2)',
required: false,
}),
headerRow: Property.Number({
displayName: 'Header Row',
description: 'Row number of the header',
required: false,
}),
firstDataRow: Property.Number({
displayName: 'First Data Row',
description: 'Row number of the first data row',
required: false,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const range = propsValue['range'];
const headerRow = propsValue['headerRow'];
const firstDataRow = propsValue['firstDataRow'];
let url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/`;
if (!range) {
url += 'usedRange(valuesOnly=true)';
} else {
url += `range(address = '${range}')`;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
const rows = response.body['values'];
if (headerRow && firstDataRow) {
return rows.slice(firstDataRow - 1).map((row: any[]) => {
const obj: { [key: string]: any } = {};
rows[headerRow - 1].forEach(
(header: any, colIndex: string | number) => {
obj[String(header)] = row[Number(colIndex)];
}
);
return obj;
});
}
return rows;
},
});

View File

@@ -0,0 +1,59 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
export const getWorksheetsAction = createAction({
auth: excelAuth,
name: 'get_worksheets',
description: 'Retrieve worksheets from a workbook',
displayName: 'Get Worksheets',
props: {
workbook: excelCommon.workbook_id,
returnAll: Property.Checkbox({
displayName: 'Return All',
description: 'If checked, all worksheets will be returned',
required: false,
defaultValue: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Limit the number of worksheets returned',
required: false,
defaultValue: 10,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook'];
const returnAll = propsValue['returnAll'];
const limit = propsValue['limit'];
const endpoint = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets`;
const headers = {
Authorization: `Bearer ${auth['access_token']}`,
'Content-Type': 'application/json',
};
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: endpoint,
headers: headers,
});
if (response.status !== 200) {
throw new Error(`Failed to retrieve worksheet: ${response.body}`);
}
const worksheets = response.body['value'];
if (returnAll) {
return worksheets;
} else {
const limitedWorksheets = [];
for (let i = 0; i < Math.min(worksheets['length'], limit ?? 0); i++) {
limitedWorksheets.push(worksheets[i]);
}
return limitedWorksheets;
}
},
});

View File

@@ -0,0 +1,88 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
} from '@activepieces/pieces-common';
import { excelAuth } from '../..';
import { excelCommon } from '../common/common';
export const lookupTableColumnAction = createAction({
auth: excelAuth,
name: 'lookup_table_column',
description: 'Lookup a value in a table column in a worksheet',
displayName: 'Lookup Table Column',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
lookup_column: Property.ShortText({
displayName: 'Lookup Column',
description: 'The column name to lookup the value in',
required: true,
}),
lookup_value: Property.ShortText({
displayName: 'Lookup Value',
description: 'The value to lookup',
required: true,
}),
return_all_matches: Property.Checkbox({
displayName: 'Return All Matches',
description: 'If checked, all matching rows will be returned',
required: false,
defaultValue: false,
}),
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const tableName = propsValue['table_id'];
const lookupColumn = propsValue['lookup_column'];
const lookupValue = propsValue['lookup_value'];
const returnAllMatches = propsValue['return_all_matches'];
const rowsUrl = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableName}/rows`;
const rowsResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: rowsUrl,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
const columnsUrl = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableName}/columns`;
const columnsResponse = await httpClient.sendRequest({
method: HttpMethod.GET,
url: columnsUrl,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth['access_token'],
},
});
const columns = columnsResponse.body['value'];
const columnIndex = columns.findIndex(
(column: any) => column.name === lookupColumn
);
if (columnIndex === -1) {
throw new Error(`Column "${lookupColumn}" not found in the table.`);
}
const rows = rowsResponse.body['value'];
const matchedRows = rows.filter(
(row: any) => row.values[0][columnIndex] === lookupValue
);
const matchedValues = matchedRows.map(
(row: { values: any[] }) => row.values[0]
);
if (returnAllMatches) {
return matchedValues;
} else {
return matchedValues[0] || null;
}
},
});

View File

@@ -0,0 +1,52 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon } from '../common/common';
const namingRules = `
The new name for the worksheet. The name must adhere to the following rules:
- Cannot be blank.
- Cannot exceed 31 characters.
- Must not contain any of the following characters: \`/\`, \`\\\`, \`?\`, \`*\`, \`:\`, \`[\`, \`]\`.
- The name "History" is reserved by Excel and cannot be used.
`;
export const renameWorksheetAction = createAction({
auth: excelAuth,
name: 'rename_worksheet',
displayName: 'Rename Worksheet',
description: 'Change the name of an existing worksheet.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
new_name: Property.ShortText({
displayName: 'New Worksheet Name',
description: namingRules,
required: true
})
},
async run(context) {
const { workbook_id, worksheet_id, new_name } = context.propsValue;
const { access_token } = context.auth;
// The worksheet_id prop from excelCommon returns the worksheet's current name,
// which can be used to identify it in the API URL.
const response = await httpClient.sendRequest({
method: HttpMethod.PATCH,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token
},
body: {
name: new_name
}
});
return response.body;
}
});

View File

@@ -0,0 +1,58 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
import { excelAuth } from '../../index';
import { excelCommon, objectToArray } from '../common/common';
export const updateRowAction = createAction({
auth: excelAuth,
name: 'update_row',
description: 'Update a row in a worksheet',
displayName: 'Update Worksheet Rows',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
row_number: Property.Number({
displayName: 'Row number',
description: 'The row number to update',
required: true,
}),
first_row_headers: Property.Checkbox({
displayName: 'Does the first row contain headers?',
description: 'If the first row is headers',
required: true,
defaultValue: false,
}),
values: excelCommon.values,
},
async run({ propsValue, auth }) {
const workbookId = propsValue['workbook_id'];
const worksheetId = propsValue['worksheet_id'];
const rowNumber = propsValue['row_number'];
const values = propsValue.first_row_headers
? objectToArray(propsValue['values'])
: Object.values(propsValue['values']);
const requestBody = {
values: [values],
};
const lastUsedColumn = excelCommon.numberToColumnName(Object.values(values).length);
const rangeFrom = `A${rowNumber}`;
const rangeTo = `${lastUsedColumn}${rowNumber}`;
const request = {
method: HttpMethod.PATCH,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/range(address='${rangeFrom}:${rangeTo}')`,
body: requestBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN as const,
token: auth['access_token'],
},
};
const response = await httpClient.sendRequest(request);
return response.body;
},
});

View File

@@ -0,0 +1,407 @@
import { Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import {
httpClient,
HttpMethod,
AuthenticationType,
HttpRequest
} from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { excelAuth } from '../..';
export const excelCommon = {
baseUrl: 'https://graph.microsoft.com/v1.0/me/drive',
workbook_id: Property.Dropdown({
auth: excelAuth,
displayName: 'Workbook',
required: true,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first'
};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
const workbooks: { id: string; name: string }[] = (
await httpClient.sendRequest<{ value: { id: string; name: string }[] }>(
{
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/root/search(q='.xlsx')?$select=id,name`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp['access_token']
}
}
)
).body.value;
return {
disabled: false,
options: workbooks.map((workbook: { id: string; name: string }) => {
return {
label: workbook.name,
value: workbook.id
};
})
};
},
refreshers: []
}),
worksheet_id: Property.Dropdown({
auth: excelAuth,
displayName: 'Worksheet',
required: true,
refreshers: ['workbook_id'],
options: async ({ auth, workbook_id }) => {
if (!auth || !workbook_id) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workbook first'
};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
const worksheets: { id: string; name: string }[] = (
await httpClient.sendRequest<{ value: { id: string; name: string }[] }>(
{
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets?$select=id,name`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp['access_token']
}
}
)
).body.value;
return {
disabled: false,
options: worksheets.map((worksheet: { id: string; name: string }) => {
return {
label: worksheet.name,
value: worksheet.name
};
})
};
}
}),
table_id: Property.Dropdown({
auth: excelAuth,
displayName: 'Table',
required: true,
refreshers: ['workbook_id', 'worksheet_id'],
options: async ({ auth, workbook_id, worksheet_id }) => {
if (!auth || !workbook_id || !worksheet_id) {
return {
disabled: true,
options: [],
placeholder: 'Please select a workbook and worksheet first'
};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
const tables: { id: string; name: string }[] = (
await httpClient.sendRequest<{ value: { id: string; name: string }[] }>(
{
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbook_id}/workbook/worksheets/${worksheet_id}/tables`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp['access_token']
}
}
)
).body.value;
return {
disabled: false,
options: tables.map((table: { id: string; name: string }) => {
return {
label: table.name,
value: table.id
};
})
};
}
}),
values: Property.DynamicProperties({
auth: excelAuth,
displayName: 'Values',
description: 'The values to insert',
required: true,
refreshers: ['workbook_id', 'worksheet_id', 'first_row_headers'],
props: async ({ auth, workbook_id, worksheet_id, first_row_headers }) => {
if (
!auth ||
(workbook_id ?? '').toString().length === 0 ||
(worksheet_id ?? '').toString().length === 0
) {
return {};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
if (!first_row_headers) {
return {
values: Property.Array({
displayName: 'Values',
required: true
})
};
}
const firstRow = await excelCommon.getHeaders(
workbook_id as unknown as string,
authProp['access_token'],
worksheet_id as unknown as string
);
const properties: {
[key: string]: any;
} = {};
for (const key in firstRow) {
properties[key] = Property.ShortText({
displayName: firstRow[key].toString(),
description: firstRow[key].toString(),
required: false,
defaultValue: ''
});
}
return properties;
}
}),
table_values: Property.DynamicProperties({
auth: excelAuth,
displayName: 'Values',
description: 'The values to insert',
required: true,
refreshers: ['workbook_id', 'worksheet_id', 'table_id'],
props: async ({ auth, workbook_id, worksheet_id, table_id }) => {
if (
!auth ||
(workbook_id ?? '').toString().length === 0 ||
(worksheet_id ?? '').toString().length === 0 ||
(worksheet_id ?? '').toString().length === 0
) {
return {};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
const headers = await excelCommon.getTableHeaders(
workbook_id as unknown as string,
authProp['access_token'],
worksheet_id as unknown as string,
table_id as unknown as string
);
const properties: {
[key: string]: any;
} = {};
for (const key in headers) {
properties[key] = Property.ShortText({
displayName: headers[key].toString(),
description: headers[key].toString(),
required: false,
defaultValue: ''
});
}
return properties;
}
}),
parent_folder: Property.Dropdown({
auth: excelAuth,
displayName: 'Parent Folder',
description: 'The parent folder to use',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please authenticate first'
};
}
const authProp: OAuth2PropertyValue = auth as OAuth2PropertyValue;
const rootFolderId = 'root';
const allFolders: { id: string; name: string }[] =
await excelCommon.getAllFolders(
rootFolderId,
authProp['access_token'],
''
);
allFolders.unshift({
id: rootFolderId,
name: '/'
});
return {
disabled: false,
options: allFolders.map((table: { id: string; name: string }) => {
return {
label: table.name,
value: table.id
};
})
};
}
}),
getHeaders: async function (
workbookId: string,
accessToken: string,
worksheetId: string
) {
const response = await httpClient.sendRequest<{
values: (string | number | boolean)[][];
}>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange(valuesOnly=true)`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken
}
});
return response.body.values?.[0] ?? [];
},
getTableHeaders: async function (
workbookId: string,
accessToken: string,
worksheetId: string,
tableId: string
) {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/tables/${tableId}/columns`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken
}
});
const columnNames = response.body['value'].map(
(column: { name: any }) => column.name
);
return columnNames;
},
getLastUsedRow: async function (
workbookId: string,
worksheetId: string,
accessToken: string
): Promise<number> {
const url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`;
const request: HttpRequest = {
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken
}
};
const response = await httpClient.sendRequest(request);
const usedRange = response.body['address'].split('!')[1];
const lastCell = usedRange.indexOf(':') != -1 ? usedRange.split(':')[1] : usedRange;
const lastRow = parseInt(lastCell.match(/\d+/)[0], 10);
return lastRow;
},
getLastUsedColumn: async function (
workbookId: string,
worksheetId: string,
accessToken: string
): Promise<string> {
const url = `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange`;
const request: HttpRequest = {
method: HttpMethod.GET,
url: url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken
}
};
const response = await httpClient.sendRequest(request);
const usedRange = response.body['address'].split('!')[1];
const [, lastCell] = usedRange.split(':');
const lastColumnLetter = lastCell.match(/[A-Z]+/)[0];
return lastColumnLetter;
},
getAllRows: async function (
workbookId: string,
worksheetId: string,
accessToken: string
): Promise<(string | number | boolean)[][]> {
const response = await httpClient.sendRequest<{
values: (string | number | boolean)[][];
}>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets/${worksheetId}/usedRange(valuesOnly=true)`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken
}
});
return response.body['values'] ?? [];
},
numberToColumnName: function (num: number): string {
let columnName = '';
while (num > 0) {
const modulo = (num - 1) % 26;
columnName = String.fromCharCode(65 + modulo) + columnName;
num = Math.floor((num - modulo) / 26);
}
return columnName;
},
getAllFolders: async function (
folderId: string,
authToken: string,
currentPath: string
): Promise<{ id: string; name: string }[]> {
const apiUrl = `${excelCommon.baseUrl}/items/${folderId}/children?$filter=folder ne null`;
const response = await httpClient.sendRequest<{
value: { id: string; name: string; folder?: unknown }[];
}>({
url: apiUrl,
method: HttpMethod.GET,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authToken
}
});
if (!response.body || !response.body.value) {
return [];
}
const folders = response.body.value.map((folder) => ({
id: `${folder.id}`,
name: `${currentPath}/${folder.name}`
}));
let allSubFolders: { id: string; name: string }[] = [...folders];
for (const folder of folders) {
const subFolders = await excelCommon.getAllFolders(
folder.id,
authToken,
folder.name
);
allSubFolders = allSubFolders.concat(subFolders);
}
return allSubFolders;
}
};
export function objectToArray(obj: { [x: string]: any }) {
const maxIndex = Math.max(...Object.keys(obj).map(Number));
const arr = new Array(maxIndex + 1).fill(null);
for (const key in obj) {
arr[Number(key)] = obj[key];
}
return arr;
}

View File

@@ -0,0 +1,112 @@
import {
AppConnectionValueForAuthProperty,
Property,
createTrigger,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { excelAuth } from '../..';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof excelAuth>,
{
workbook_id: string;
worksheet_id: string;
max_rows_to_poll: number | undefined;
}
> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, lastItemId }) => {
const fetchedValues =
(await excelCommon.getAllRows(
propsValue.workbook_id,
propsValue.worksheet_id,
auth.access_token
)) ?? [];
const currentValues = fetchedValues.map((row: any[], rowIndex: number) => {
const rowObject: any = {};
row.forEach((cell: any, cellIndex: number) => {
const columnName = String.fromCharCode(65 + cellIndex);
rowObject[columnName] = cell;
});
return {
row: rowIndex + 1,
values: rowObject,
};
});
const items = currentValues
.filter((f: any) => Object.keys(f.values).length > 0)
.map((item: any, index: number) => ({
id: index + 1,
data: item,
}))
.filter(
(f: any) => isNil(lastItemId) || f.data.row > (lastItemId as number)
);
return items.reverse();
},
};
export const readNewRows = createTrigger({
auth: excelAuth,
name: 'new_row',
displayName: 'New Row',
description:
'Trigger when a new row is added, and it can include existing rows as well.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
max_rows_to_poll: Property.Number({
displayName: 'Max Rows to Poll',
description:
'The maximum number of rows to poll, the rest will be polled on the next run.',
required: false,
defaultValue: 10,
}),
},
type: TriggerStrategy.POLLING,
sampleData: {},
onEnable: async (context) => {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
onDisable: async (context) => {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
run: async (context) => {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
maxItemsToPoll: Math.max(
1,
Math.min(10, context.propsValue.max_rows_to_poll ?? 10)
),
propsValue: context.propsValue,
files: context.files,
});
},
test: async (context) => {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,170 @@
import {
AppConnectionValueForAuthProperty,
OAuth2PropertyValue,
Property,
createTrigger,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import {
DedupeStrategy,
Polling,
pollingHelper,
httpClient,
HttpMethod,
AuthenticationType
} from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { excelAuth } from '../..';
interface TableRow {
index: number;
values: [[string | number | boolean]];
}
// Helper function to get all rows from a specific table
async function getTableRows(auth: OAuth2PropertyValue, workbookId: string, tableId: string): Promise<TableRow[]> {
try {
const response = await httpClient.sendRequest<{ value: TableRow[] }>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/tables/${encodeURIComponent(tableId)}/rows`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
});
return response.body.value ?? [];
} catch (error) {
throw new Error(`Failed to fetch table rows: ${error}`);
}
}
const polling: Polling<
AppConnectionValueForAuthProperty<typeof excelAuth>,
{
workbook_id: string;
worksheet_id: string;
table_id: string;
has_headers: boolean;
}
> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, lastItemId, store }) => {
const rows = await getTableRows(auth, propsValue.workbook_id, propsValue.table_id);
if (rows.length === 0) {
return [];
}
const cachedHeaders = await store.get<string[]>('table_headers');
let headers: string[] = [];
if (cachedHeaders && cachedHeaders.length > 0) {
headers = cachedHeaders
} else {
try {
headers = await excelCommon.getTableHeaders(
propsValue.workbook_id,
auth.access_token,
propsValue.worksheet_id,
propsValue.table_id
);
await store.put('table_headers', headers);
} catch (error) {
headers = []
}
}
const processedRows = rows.map(row => {
let rowData: Record<string, unknown> = {};
if (propsValue.has_headers && headers.length > 0) {
// Map values to header keys
rowData = headers.reduce((acc, header, index) => {
acc[header] = row.values[0]?.[index] ?? null;
return acc;
}, {} as Record<string, unknown>);
} else {
// Use default column letter keys (A, B, C...)
rowData = row.values[0]?.reduce((acc, value, index) => {
acc[excelCommon.numberToColumnName(index + 1)] = value;
return acc;
}, {} as Record<string, unknown>) ?? {};
}
return {
id: row.index, // The row's zero-based index is its unique ID
data: {
rowIndex: row.index,
values: rowData
}
};
});
// The polling helper will filter for new rows where the ID (row.index) is greater than lastItemId
const newItems = processedRows.filter(item => isNil(lastItemId) || item.id > (lastItemId as number));
return newItems;
}
};
export const newRowInTableTrigger = createTrigger({
auth: excelAuth,
name: 'new_row_in_table',
displayName: 'New Row in Table',
description: 'Fires when a new row is added to a table within a worksheet.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
table_id: excelCommon.table_id,
has_headers: Property.Checkbox({
displayName: "My table has headers",
description: "Enable this if the first row of your table is a header row.",
required: true,
defaultValue: true,
})
},
type: TriggerStrategy.POLLING,
sampleData: {
"rowIndex": 0,
"values": {
"ID": 1,
"Name": "John Doe",
"Email": "john.doe@example.com"
}
},
onEnable: async (context) => {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
onDisable: async (context) => {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
run: async (context) => {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
test: async (context) => {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,118 @@
import {
AppConnectionValueForAuthProperty,
OAuth2PropertyValue,
createTrigger
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import { excelAuth } from '../..';
import {
httpClient,
HttpMethod,
AuthenticationType,
Polling,
pollingHelper,
DedupeStrategy
} from '@activepieces/pieces-common';
interface Worksheet {
id: string;
name: string;
position: number;
visibility: string;
}
async function getWorksheets(
auth: OAuth2PropertyValue,
workbookId: string
): Promise<Worksheet[]> {
if (!workbookId) return [];
try {
const response = await httpClient.sendRequest<{ value: Worksheet[] }>({
method: HttpMethod.GET,
url: `${excelCommon.baseUrl}/items/${workbookId}/workbook/worksheets`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token
}
});
return response.body.value ?? [];
} catch (error) {
throw new Error(`Failed to fetch worksheets: ${error}`);
}
}
const polling: Polling<AppConnectionValueForAuthProperty<typeof excelAuth>, { workbook_id: string }> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, store }) => {
const worksheets = await getWorksheets(auth, propsValue.workbook_id);
const storedWorksheetIds = await store.get<string[]>('worksheet_ids') ?? [];
const newWorksheets = worksheets.filter(ws => !storedWorksheetIds.includes(ws.id));
const currentWorksheetIds = worksheets.map(ws => ws.id);
await store.put('worksheet_ids', currentWorksheetIds);
const processedWorksheets = newWorksheets.map((worksheet) => ({
id: worksheet.id,
data: worksheet
}));
return processedWorksheets;
}
};
export const newWorksheetTrigger = createTrigger({
auth: excelAuth,
name: 'new_worksheet',
displayName: 'New Worksheet',
description: 'Fires when a new worksheet is created in a workbook.',
props: {
workbook_id: excelCommon.workbook_id
},
type: TriggerStrategy.POLLING,
sampleData: {
'@odata.id':
'/workbook/worksheets(%27%7B00000000-0001-0000-0100-000000000000%7D%27)',
id: '{00000000-0001-0000-0100-000000000000}',
name: 'Sheet2',
position: 1,
visibility: 'Visible'
},
onEnable: async (context) => {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue
});
},
onDisable: async (context) => {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue
});
},
run: async (context) => {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files
});
},
test: async (context) => {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files
});
}
});

View File

@@ -0,0 +1,155 @@
import {
AppConnectionValueForAuthProperty,
Property,
createTrigger,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { excelCommon } from '../common/common';
import { excelAuth } from '../..';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { createHmac } from 'node:crypto';
const triggerName = 'updated_row';
function createRowHash(rowData: unknown[]): string {
const rowString = JSON.stringify(rowData);
return createHmac('sha1', 'activepieces').update(rowString).digest('hex');
}
// Helper function to get all worksheet rows with error handling
async function getWorksheetRows(auth: AppConnectionValueForAuthProperty<typeof excelAuth>, workbookId: string, worksheetId: string): Promise<(string | number | boolean)[][]> {
try {
return await excelCommon.getAllRows(workbookId, worksheetId, auth.access_token);
} catch (error) {
throw new Error(`Failed to fetch worksheet rows: ${error}`);
}
}
// Polling implementation using the framework's best practices
const polling: Polling<
AppConnectionValueForAuthProperty<typeof excelAuth>,
{
workbook_id: string;
worksheet_id: string;
has_headers: boolean;
}
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, store }) => {
const allRows = await getWorksheetRows(auth, propsValue.workbook_id, propsValue.worksheet_id);
if (allRows.length === 0) {
return [];
}
// Get stored row hashes from previous run
const oldHashes = await store.get<Record<number, string>>('row_hashes') ?? {};
const headers = (propsValue.has_headers && allRows.length > 0) ? allRows[0] : [];
const dataRows = (propsValue.has_headers && allRows.length > 0) ? allRows.slice(1) : allRows;
const currentHashes: Record<number, string> = {};
const changedItems: Array<{ epochMilliSeconds: number; data: unknown }> = [];
const currentTime = Date.now();
// Process each data row
dataRows.forEach((row, index) => {
const rowIndex = propsValue.has_headers ? index + 1 : index;
const newHash = createRowHash(row);
currentHashes[rowIndex] = newHash;
const oldHash = oldHashes[rowIndex];
// Row has changed or is new
if (oldHash !== newHash) {
const formattedRow: Record<string, unknown> = {};
if (propsValue.has_headers && headers.length > 0) {
headers.forEach((header, colIndex) => {
formattedRow[String(header)] = row[colIndex] ?? null;
});
} else {
row.forEach((cell, colIndex) => {
formattedRow[excelCommon.numberToColumnName(colIndex + 1)] = cell;
});
}
changedItems.push({
epochMilliSeconds: currentTime,
data: {
rowIndex: rowIndex + 1, // Make it 1-based for user readability
values: formattedRow,
changeType: oldHash ? 'updated' : 'added'
}
});
}
});
// Update stored hashes for next run
await store.put('row_hashes', currentHashes);
return changedItems;
}
};
export const updatedRowTrigger = createTrigger({
auth: excelAuth,
name: triggerName,
displayName: 'Updated Row',
description: 'Fires when a row (in a worksheet) is added or updated.',
props: {
workbook_id: excelCommon.workbook_id,
worksheet_id: excelCommon.worksheet_id,
has_headers: Property.Checkbox({
displayName: "First row has headers",
description: "Enable this if the first row of your worksheet should be treated as headers.",
required: true,
defaultValue: false,
})
},
type: TriggerStrategy.POLLING,
sampleData: {
"rowIndex": 1,
"values": { "ID": 101, "Product": "Widget", "Price": 19.99 }
},
onEnable: async (context) => {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
onDisable: async (context) => {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
run: async (context) => {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
test: async (context) => {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});