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,102 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon, addRowToSmartsheet } from '../common';
export const addRowToSheet = createAction({
auth: smartsheetAuth,
name: 'add_row_to_sheet',
displayName: 'Add Row to Sheet',
description:'Adds new row to a sheet.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
cells: smartsheetCommon.cells,
location_type: Property.StaticDropdown({
displayName: 'Add Row to Top or Bottom',
required: true,
defaultValue: 'bottom',
options: {
options: [
{ label: 'Top of sheet', value: 'top' },
{ label: 'Bottom of sheet', value: 'bottom' },
],
},
}),
},
async run(context) {
const { sheet_id, cells, location_type } = context.propsValue;
// Transform dynamic cells data into proper Smartsheet format
const cellsData = cells as Record<string, any>;
const transformedCells: any[] = [];
for (const [key, value] of Object.entries(cellsData)) {
if (value === undefined || value === null || value === '') {
continue; // Skip empty values
}
let columnId: number;
const cellObj: any = {};
if (key.startsWith('column_')) {
// Regular column value
columnId = parseInt(key.replace('column_', ''));
cellObj.columnId = columnId;
cellObj.value = value;
} else {
continue; // Skip unknown keys
}
transformedCells.push(cellObj);
}
if (transformedCells.length === 0) {
throw new Error('At least one cell value must be provided');
}
// Build the row object with location specifiers
const rowObj: any = {
cells: transformedCells,
};
// Add location specifiers based on location_type
switch (location_type) {
case 'top':
rowObj.toTop = true;
break;
case 'bottom':
rowObj.toBottom = true;
break;
}
const rowPayload = [rowObj];
try {
const result = await addRowToSmartsheet(
context.auth.secret_text,
sheet_id as string,
rowPayload,
);
return {
success: true,
row: result,
message: 'Row added successfully',
cells_processed: transformedCells.length,
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid row data or parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to add rows to this sheet');
} else if (error.response?.status === 404) {
throw new Error('Sheet not found or you do not have access to it');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to add row: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,204 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon } from '../common';
export const attachFileToRow = createAction({
auth: smartsheetAuth,
name: 'attach_file_to_row',
displayName: 'Attach File to Row',
description: 'Adds a file attachment to a row.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
row_id: smartsheetCommon.row_id,
attachment_type: Property.StaticDropdown({
displayName: 'Attachment Type',
description: 'Type of attachment to add',
required: true,
defaultValue: 'FILE',
options: {
options: [
{ label: 'File Upload', value: 'FILE' },
{ label: 'URL Link', value: 'LINK' },
{ label: 'Box.com', value: 'BOX_COM' },
{ label: 'Dropbox', value: 'DROPBOX' },
{ label: 'Egnyte', value: 'EGNYTE' },
{ label: 'Evernote', value: 'EVERNOTE' },
{ label: 'Google Drive', value: 'GOOGLE_DRIVE' },
{ label: 'OneDrive', value: 'ONEDRIVE' },
],
},
}),
// For file uploads
file: Property.File({
displayName: 'File',
description: 'The file to attach (required for FILE type)',
required: false,
}),
// For URL attachments
url: Property.ShortText({
displayName: 'URL',
description: 'The URL to attach (required for URL-based attachment types)',
required: false,
}),
attachment_name: Property.ShortText({
displayName: 'Attachment Name',
description: 'Name for the attachment (optional, will use file name or URL if not provided)',
required: false,
}),
// Advanced options
attachment_sub_type: Property.StaticDropdown({
displayName: 'Attachment Sub Type',
description: 'Sub type for Google Drive and Egnyte attachments',
required: false,
options: {
options: [
{ label: 'Document', value: 'DOCUMENT' },
{ label: 'Drawing', value: 'DRAWING' },
{ label: 'Folder', value: 'FOLDER' },
{ label: 'PDF', value: 'PDF' },
{ label: 'Presentation', value: 'PRESENTATION' },
{ label: 'Spreadsheet', value: 'SPREADSHEET' },
],
},
}),
mime_type: Property.ShortText({
displayName: 'MIME Type',
description: 'MIME type of the attachment (optional, auto-detected for files)',
required: false,
}),
},
async run(context) {
const {
sheet_id,
row_id,
attachment_type,
file,
url,
attachment_name,
attachment_sub_type,
mime_type,
} = context.propsValue;
// Validate input based on attachment type
if (attachment_type === 'FILE') {
if (!file) {
throw new Error('File is required when attachment type is FILE');
}
} else {
if (!url) {
throw new Error('URL is required for URL-based attachment types');
}
// Validate URL format for specific types
if (attachment_type === 'BOX_COM' && !url.includes('box.com')) {
throw new Error('Box.com URLs should contain "box.com" in the domain');
}
if (attachment_type === 'DROPBOX' && !url.includes('dropbox.com')) {
throw new Error('Dropbox URLs should contain "dropbox.com" in the domain');
}
if (attachment_type === 'GOOGLE_DRIVE' && !url.includes('drive.google.com')) {
throw new Error('Google Drive URLs should contain "drive.google.com" in the domain');
}
if (attachment_type === 'ONEDRIVE' && !url.includes('onedrive')) {
throw new Error('OneDrive URLs should contain "onedrive" in the domain');
}
}
const apiUrl = `${smartsheetCommon.baseUrl}/sheets/${sheet_id}/rows/${row_id}/attachments`;
try {
let request: HttpRequest;
if (attachment_type === 'FILE') {
// File upload using multipart/form-data
const formData = new FormData();
// Determine MIME type
const fileMimeType = mime_type || (file?.extension ? `application/${file.extension}` : 'application/octet-stream');
const fileName = attachment_name || file?.filename || 'attachment';
// Create blob with proper MIME type
const blob = new Blob([file!.data as unknown as ArrayBuffer], { type: fileMimeType });
formData.append('file', blob, fileName);
request = {
method: HttpMethod.POST,
url: apiUrl,
headers: {
'Authorization': `Bearer ${context.auth}`,
// Don't set Content-Type for FormData, let the browser set it with boundary
},
body: formData,
};
} else {
// URL attachment using JSON
const attachmentData: any = {
attachmentType: attachment_type,
url: url,
};
if (attachment_name) {
attachmentData.name = attachment_name;
}
if (attachment_sub_type) {
attachmentData.attachmentSubType = attachment_sub_type;
}
if (mime_type) {
attachmentData.mimeType = mime_type;
}
request = {
method: HttpMethod.POST,
url: apiUrl,
headers: {
'Authorization': `Bearer ${context.auth}`,
'Content-Type': 'application/json',
},
body: attachmentData,
};
}
const response = await httpClient.sendRequest(request);
return {
success: true,
attachment: response.body.result,
message: 'Attachment added successfully',
attachment_id: response.body.result?.id,
attachment_type: response.body.result?.attachmentType,
attachment_name: response.body.result?.name,
size_kb: response.body.result?.sizeInKb,
created_at: response.body.result?.createdAt,
created_by: response.body.result?.createdBy,
version: response.body.version,
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid attachment data or parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to add attachments to this sheet');
} else if (error.response?.status === 404) {
throw new Error('Sheet or row not found or you do not have access to it');
} else if (error.response?.status === 413) {
throw new Error('File size too large. Check Smartsheet file size limits for your plan.');
} else if (error.response?.status === 415) {
throw new Error('Unsupported media type. Check file format restrictions.');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to attach file: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,259 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon } from '../common';
export const findAttachmentByRowId = createAction({
auth: smartsheetAuth,
name: 'find_attachment_by_row_id',
displayName: 'List Row Attachments',
description: 'Get all attachments for a specific row in a Smartsheet, including row and discussion-level attachments with comprehensive pagination and filtering options',
props: {
sheet_id: smartsheetCommon.sheet_id(),
row_id: smartsheetCommon.row_id,
// Pagination options
include_all: Property.Checkbox({
displayName: 'Include All Results',
description: 'If true, include all results without pagination (overrides page and page size)',
required: false,
defaultValue: false,
}),
page: Property.Number({
displayName: 'Page Number',
description: 'Which page to return (defaults to 1, ignored if "Include All Results" is true)',
required: false,
defaultValue: 1,
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Maximum number of items to return per page (defaults to 100, max 10000, ignored if "Include All Results" is true)',
required: false,
defaultValue: 100,
}),
// Filtering options
attachment_type_filter: Property.StaticMultiSelectDropdown({
displayName: 'Filter by Attachment Type',
description: 'Only return attachments of specific types (leave empty for all types)',
required: false,
options: {
options: [
{ label: 'Files', value: 'FILE' },
{ label: 'URLs/Links', value: 'LINK' },
{ label: 'Box.com', value: 'BOX_COM' },
{ label: 'Dropbox', value: 'DROPBOX' },
{ label: 'Egnyte', value: 'EGNYTE' },
{ label: 'Evernote', value: 'EVERNOTE' },
{ label: 'Google Drive', value: 'GOOGLE_DRIVE' },
{ label: 'OneDrive', value: 'ONEDRIVE' },
{ label: 'Trello', value: 'TRELLO' },
],
},
}),
parent_type_filter: Property.StaticMultiSelectDropdown({
displayName: 'Filter by Parent Type',
description: 'Only return attachments from specific parent types (leave empty for all)',
required: false,
options: {
options: [
{ label: 'Row Attachments', value: 'ROW' },
{ label: 'Comment Attachments', value: 'COMMENT' },
{ label: 'Sheet Attachments', value: 'SHEET' },
{ label: 'Proof Attachments', value: 'PROOF' },
],
},
}),
min_file_size_kb: Property.Number({
displayName: 'Minimum File Size (KB)',
description: 'Only return files with size greater than or equal to this value (applies to FILE type only)',
required: false,
}),
max_file_size_kb: Property.Number({
displayName: 'Maximum File Size (KB)',
description: 'Only return files with size less than or equal to this value (applies to FILE type only)',
required: false,
}),
},
async run(context) {
const {
sheet_id,
row_id,
include_all,
page,
page_size,
attachment_type_filter,
parent_type_filter,
min_file_size_kb,
max_file_size_kb,
} = context.propsValue;
// Build query parameters
const queryParams: any = {};
if (include_all) {
queryParams.includeAll = true;
} else {
if (page && page > 1) {
queryParams.page = page;
}
if (page_size && page_size !== 100) {
queryParams.pageSize = Math.min(page_size, 10000); // Cap at API limit
}
}
const apiUrl = `${smartsheetCommon.baseUrl}/sheets/${sheet_id}/rows/${row_id}/attachments`;
try {
const request: HttpRequest = {
method: HttpMethod.GET,
url: apiUrl,
headers: {
'Authorization': `Bearer ${context.auth}`,
'Content-Type': 'application/json',
},
queryParams,
};
const response = await httpClient.sendRequest(request);
const attachmentData = response.body;
// Apply client-side filters
let filteredAttachments = attachmentData.data || [];
// Filter by attachment type
if (attachment_type_filter && attachment_type_filter.length > 0) {
filteredAttachments = filteredAttachments.filter((attachment: any) =>
attachment_type_filter.includes(attachment.attachmentType)
);
}
// Filter by parent type
if (parent_type_filter && parent_type_filter.length > 0) {
filteredAttachments = filteredAttachments.filter((attachment: any) =>
parent_type_filter.includes(attachment.parentType)
);
}
// Filter by file size (only applies to FILE type)
if (min_file_size_kb !== undefined || max_file_size_kb !== undefined) {
filteredAttachments = filteredAttachments.filter((attachment: any) => {
if (attachment.attachmentType !== 'FILE' || !attachment.sizeInKb) {
return true;
}
const size = attachment.sizeInKb;
if (min_file_size_kb !== undefined && size < min_file_size_kb) {
return false;
}
if (max_file_size_kb !== undefined && size > max_file_size_kb) {
return false;
}
return true;
});
}
// Organize attachments by type for better analysis
const attachmentsByType: any = {};
const attachmentsByParent: any = {};
let totalFileSize = 0;
filteredAttachments.forEach((attachment: any) => {
// Group by attachment type
if (!attachmentsByType[attachment.attachmentType]) {
attachmentsByType[attachment.attachmentType] = [];
}
attachmentsByType[attachment.attachmentType].push(attachment);
// Group by parent type
if (!attachmentsByParent[attachment.parentType]) {
attachmentsByParent[attachment.parentType] = [];
}
attachmentsByParent[attachment.parentType].push(attachment);
// Calculate total file size for files
if (attachment.attachmentType === 'FILE' && attachment.sizeInKb) {
totalFileSize += attachment.sizeInKb;
}
});
return {
success: true,
// Pagination info
pagination: {
page_number: attachmentData.pageNumber,
page_size: attachmentData.pageSize,
total_pages: attachmentData.totalPages,
total_count: attachmentData.totalCount,
filtered_count: filteredAttachments.length,
},
// Main results
attachments: filteredAttachments,
// Organized results
attachments_by_type: attachmentsByType,
attachments_by_parent: attachmentsByParent,
// Summary statistics
summary: {
total_attachments: filteredAttachments.length,
files_count: (attachmentsByType.FILE || []).length,
links_count: (attachmentsByType.LINK || []).length,
cloud_storage_count: filteredAttachments.length -
(attachmentsByType.FILE || []).length -
(attachmentsByType.LINK || []).length,
row_attachments: (attachmentsByParent.ROW || []).length,
comment_attachments: (attachmentsByParent.COMMENT || []).length,
total_file_size_kb: totalFileSize,
total_file_size_mb: Math.round(totalFileSize / 1024 * 100) / 100,
},
// Download info for files
download_info: filteredAttachments
.filter((att: any) => att.attachmentType === 'FILE' && att.url)
.map((att: any) => ({
attachment_id: att.id,
name: att.name,
download_url: att.url,
url_expires_in_millis: att.urlExpiresInMillis,
url_expires_at: att.urlExpiresInMillis ?
new Date(Date.now() + att.urlExpiresInMillis).toISOString() : null,
size_kb: att.sizeInKb,
})),
// Applied filters info
filters_applied: {
attachment_types: attachment_type_filter || [],
parent_types: parent_type_filter || [],
min_file_size_kb: min_file_size_kb,
max_file_size_kb: max_file_size_kb,
},
// Row and sheet info
row_id: row_id,
sheet_id: sheet_id,
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid request parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to access attachments for this row');
} else if (error.response?.status === 404) {
throw new Error('Sheet or row not found, or you do not have access to it');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to retrieve attachments: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,213 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon } from '../common';
export const findRowsByQuery = createAction({
auth: smartsheetAuth,
name: 'find_rows_by_query',
displayName: 'Find Row',
description: 'Finds rows in a specific sheet or across all accessible sheets using text queries with advanced filtering options.',
props: {
search_scope: Property.StaticDropdown({
displayName: 'Search Scope',
description: 'Choose whether to search within a specific sheet or across all accessible sheets.',
required: true,
defaultValue: 'specific_sheet',
options: {
options: [
{ label: 'Specific Sheet', value: 'specific_sheet' },
{ label: 'All Accessible Sheets', value: 'all_sheets' },
],
},
}),
sheet_id: smartsheetCommon.sheet_id(false),
query: Property.ShortText({
displayName: 'Search Query',
description: 'Text to search for. Use double quotes for exact phrase matching (e.g., "project status")',
required: true,
}),
// Advanced search options
search_scopes: Property.StaticMultiSelectDropdown({
displayName: 'Search Scopes',
description: 'Specify what types of content to search in (leave empty to search all)',
required: false,
options: {
options: [
{ label: 'Cell Data', value: 'cellData' },
{ label: 'Comments', value: 'comments' },
{ label: 'Attachments', value: 'attachments' },
{ label: 'Sheet Names', value: 'sheetNames' },
{ label: 'Folder Names', value: 'folderNames' },
{ label: 'Report Names', value: 'reportNames' },
{ label: 'Dashboard Names', value: 'sightNames' },
{ label: 'Template Names', value: 'templateNames' },
{ label: 'Workspace Names', value: 'workspaceNames' },
{ label: 'Summary Fields', value: 'summaryFields' },
],
},
}),
include_favorites: Property.Checkbox({
displayName: 'Include Favorite Flags',
description: 'Include information about which items are marked as favorites',
required: false,
defaultValue: false,
}),
modified_since: Property.DateTime({
displayName: 'Modified Since',
description: 'Only return results modified on or after this date/time',
required: false,
}),
max_results: Property.Number({
displayName: 'Max Results',
description: 'Maximum number of results to return (default: 50, max: 100)',
required: false,
defaultValue: 50,
}),
object_types_filter: Property.StaticMultiSelectDropdown({
displayName: 'Filter by Object Types',
description: 'Only return results of specific object types (leave empty for all types)',
required: false,
options: {
options: [
{ label: 'Rows', value: 'row' },
{ label: 'Sheets', value: 'sheet' },
{ label: 'Attachments', value: 'attachment' },
{ label: 'Comments/Discussions', value: 'discussion' },
{ label: 'Dashboards', value: 'dashboard' },
{ label: 'Reports', value: 'report' },
{ label: 'Folders', value: 'folder' },
{ label: 'Templates', value: 'template' },
{ label: 'Workspaces', value: 'workspace' },
{ label: 'Summary Fields', value: 'summaryField' },
],
},
}),
},
async run(context) {
const {
search_scope,
sheet_id,
query,
search_scopes,
include_favorites,
modified_since,
max_results,
object_types_filter,
} = context.propsValue;
// Validate sheet_id requirement for specific sheet search
if (search_scope === 'specific_sheet' && !sheet_id) {
throw new Error('Sheet ID is required when searching within a specific sheet');
}
// Build query parameters
const queryParams: any = {
query: query,
};
// Add search scopes if specified
if (search_scopes && search_scopes.length > 0) {
queryParams.scopes = search_scopes;
}
// Add include favorites flag
if (include_favorites) {
queryParams.include = 'favoriteFlag';
}
// Add modified since filter
if (modified_since) {
queryParams.modifiedSince = new Date(modified_since as string).toISOString();
}
// Determine API endpoint
let apiUrl: string;
if (search_scope === 'specific_sheet') {
apiUrl = `${smartsheetCommon.baseUrl}/search/sheets/${sheet_id}`;
} else {
apiUrl = `${smartsheetCommon.baseUrl}/search`;
}
try {
const request: HttpRequest = {
method: HttpMethod.GET,
url: apiUrl,
headers: {
'Authorization': `Bearer ${context.auth}`,
'Content-Type': 'application/json',
},
queryParams,
};
const response = await httpClient.sendRequest(request);
const searchResults = response.body;
// Filter by object types if specified
let filteredResults = searchResults.results || [];
if (object_types_filter && object_types_filter.length > 0) {
filteredResults = filteredResults.filter((result: any) =>
object_types_filter.includes(result.objectType)
);
}
// Limit results if specified
const maxResults = Math.min(max_results || 50, 100);
if (filteredResults.length > maxResults) {
filteredResults = filteredResults.slice(0, maxResults);
}
// Organize results by type for better usability
const resultsByType: any = {};
filteredResults.forEach((result: any) => {
if (!resultsByType[result.objectType]) {
resultsByType[result.objectType] = [];
}
resultsByType[result.objectType].push(result);
});
return {
success: true,
total_count: searchResults.totalCount,
returned_count: filteredResults.length,
search_query: query,
search_scope: search_scope,
results: filteredResults,
results_by_type: resultsByType,
sheet_searched: search_scope === 'specific_sheet' ? sheet_id : 'all_accessible_sheets',
// Summary statistics
summary: {
rows_found: (resultsByType.row || []).length,
sheets_found: (resultsByType.sheet || []).length,
attachments_found: (resultsByType.attachment || []).length,
discussions_found: (resultsByType.discussion || []).length,
other_objects_found: filteredResults.length -
(resultsByType.row || []).length -
(resultsByType.sheet || []).length -
(resultsByType.attachment || []).length -
(resultsByType.discussion || []).length,
},
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid request parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to access sheets listing');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to retrieve sheets: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,395 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon } from '../common';
export const findSheetByName = createAction({
auth: smartsheetAuth,
name: 'find_sheet_by_name',
displayName: 'Find Sheet(s)',
description: 'Fetches existings sheets matching provided filter criteria.',
props: {
// Search options
sheet_name: Property.ShortText({
displayName: 'Sheet Name Filter',
description: 'Filter sheets by name (partial or exact match). Leave empty to list all sheets.',
required: false,
}),
exact_match: Property.Checkbox({
displayName: 'Exact Name Match',
description: 'When filtering by name, require exact match instead of partial match',
required: false,
defaultValue: false,
}),
// Pagination options
include_all: Property.Checkbox({
displayName: 'Include All Results',
description: 'If true, include all results without pagination (overrides page and page size)',
required: false,
defaultValue: false,
}),
page: Property.Number({
displayName: 'Page Number',
description: 'Which page to return (defaults to 1, ignored if "Include All Results" is true)',
required: false,
defaultValue: 1,
}),
page_size: Property.Number({
displayName: 'Page Size',
description: 'Maximum number of items to return per page (defaults to 100, max 10000, ignored if "Include All Results" is true)',
required: false,
defaultValue: 100,
}),
// Access and filtering options
access_api_level: Property.StaticDropdown({
displayName: 'Access API Level',
description: 'API access level for viewing and filtering permissions',
required: false,
defaultValue: '0',
options: {
options: [
{ label: 'Viewer (default)', value: '0' },
{ label: 'Commenter', value: '1' },
],
},
}),
access_level_filter: Property.StaticMultiSelectDropdown({
displayName: 'Filter by Access Level',
description: 'Only return sheets where you have specific access levels (leave empty for all)',
required: false,
options: {
options: [
{ label: 'Owner', value: 'OWNER' },
{ label: 'Admin', value: 'ADMIN' },
{ label: 'Editor (with sharing)', value: 'EDITOR_SHARE' },
{ label: 'Editor', value: 'EDITOR' },
{ label: 'Commenter', value: 'COMMENTER' },
{ label: 'Viewer', value: 'VIEWER' },
],
},
}),
modified_since: Property.DateTime({
displayName: 'Modified Since',
description: 'Only return sheets modified on or after this date/time',
required: false,
}),
// Additional data options
include_sheet_version: Property.Checkbox({
displayName: 'Include Sheet Version',
description: 'Include current version number of each sheet',
required: false,
defaultValue: false,
}),
include_source_info: Property.Checkbox({
displayName: 'Include Source Information',
description: 'Include information about the source (template/sheet) each sheet was created from',
required: false,
defaultValue: false,
}),
numeric_dates: Property.Checkbox({
displayName: 'Numeric Dates',
description: 'Return dates as milliseconds since UNIX epoch instead of ISO strings',
required: false,
defaultValue: false,
}),
// Advanced filtering
created_date_range: Property.StaticDropdown({
displayName: 'Created Date Range',
description: 'Filter sheets by creation date range',
required: false,
options: {
options: [
{ label: 'All time', value: 'all' },
{ label: 'Last 7 days', value: 'week' },
{ label: 'Last 30 days', value: 'month' },
{ label: 'Last 90 days', value: 'quarter' },
{ label: 'Last 365 days', value: 'year' },
],
},
}),
sort_by: Property.StaticDropdown({
displayName: 'Sort Results By',
description: 'How to sort the returned sheets',
required: false,
defaultValue: 'name',
options: {
options: [
{ label: 'Sheet Name', value: 'name' },
{ label: 'Creation Date (newest first)', value: 'created_desc' },
{ label: 'Creation Date (oldest first)', value: 'created_asc' },
{ label: 'Modified Date (newest first)', value: 'modified_desc' },
{ label: 'Modified Date (oldest first)', value: 'modified_asc' },
{ label: 'Access Level', value: 'access' },
],
},
}),
},
async run(context) {
const {
sheet_name,
exact_match,
include_all,
page,
page_size,
access_api_level,
access_level_filter,
modified_since,
include_sheet_version,
include_source_info,
numeric_dates,
created_date_range,
sort_by,
} = context.propsValue;
// Build query parameters
const queryParams: any = {};
// Pagination
if (include_all) {
queryParams.includeAll = true;
} else {
if (page && page > 1) {
queryParams.page = page;
}
if (page_size && page_size !== 100) {
queryParams.pageSize = Math.min(page_size, 10000); // Cap at API limit
}
}
// Access level
if (access_api_level && access_api_level !== '0') {
queryParams.accessApiLevel = parseInt(access_api_level as string);
}
// Modified since filter
if (modified_since) {
queryParams.modifiedSince = new Date(modified_since as string).toISOString();
}
// Include options
const includeOptions: string[] = [];
if (include_sheet_version) {
includeOptions.push('sheetVersion');
}
if (include_source_info) {
includeOptions.push('source');
}
if (includeOptions.length > 0) {
queryParams.include = includeOptions.join(',');
}
// Numeric dates
if (numeric_dates) {
queryParams.numericDates = true;
}
const apiUrl = `${smartsheetCommon.baseUrl}/sheets`;
try {
const request: HttpRequest = {
method: HttpMethod.GET,
url: apiUrl,
headers: {
'Authorization': `Bearer ${context.auth}`,
'Content-Type': 'application/json',
},
queryParams,
};
const response = await httpClient.sendRequest(request);
const sheetData = response.body;
// Apply client-side filters
let filteredSheets = sheetData.data || [];
// Filter by sheet name if specified
if (sheet_name) {
const searchName = (sheet_name as string).toLowerCase();
filteredSheets = filteredSheets.filter((sheet: any) => {
const sheetName = sheet.name.toLowerCase();
return exact_match ?
sheetName === searchName :
sheetName.includes(searchName);
});
}
// Filter by access level
if (access_level_filter && access_level_filter.length > 0) {
filteredSheets = filteredSheets.filter((sheet: any) =>
access_level_filter.includes(sheet.accessLevel)
);
}
// Filter by creation date range
if (created_date_range && created_date_range !== 'all') {
const now = new Date();
const cutoffDate = new Date();
switch (created_date_range) {
case 'week': {
cutoffDate.setDate(now.getDate() - 7);
break;
}
case 'month': {
cutoffDate.setDate(now.getDate() - 30);
break;
}
case 'quarter': {
cutoffDate.setDate(now.getDate() - 90);
break;
}
case 'year': {
cutoffDate.setDate(now.getDate() - 365);
break;
}
}
filteredSheets = filteredSheets.filter((sheet: any) => {
const createdDate = new Date(sheet.createdAt);
return createdDate >= cutoffDate;
});
}
// Sort results
if (sort_by) {
filteredSheets.sort((a: any, b: any) => {
switch (sort_by) {
case 'name':
return a.name.localeCompare(b.name);
case 'created_desc':
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
case 'created_asc':
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
case 'modified_desc':
return new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
case 'modified_asc':
return new Date(a.modifiedAt).getTime() - new Date(b.modifiedAt).getTime();
case 'access': {
const accessOrder = ['OWNER', 'ADMIN', 'EDITOR_SHARE', 'EDITOR', 'COMMENTER', 'VIEWER'];
return accessOrder.indexOf(a.accessLevel) - accessOrder.indexOf(b.accessLevel);
}
default:
return 0;
}
});
}
// Organize sheets by access level for analysis
const sheetsByAccess: any = {};
const sheetsBySource: any = {};
filteredSheets.forEach((sheet: any) => {
// Group by access level
if (!sheetsByAccess[sheet.accessLevel]) {
sheetsByAccess[sheet.accessLevel] = [];
}
sheetsByAccess[sheet.accessLevel].push(sheet);
// Group by source type (if source info is included)
if (sheet.source) {
const sourceType = sheet.source.type || 'unknown';
if (!sheetsBySource[sourceType]) {
sheetsBySource[sourceType] = [];
}
sheetsBySource[sourceType].push(sheet);
}
});
// Calculate date-based statistics
const now = new Date();
const recentlyModified = filteredSheets.filter((sheet: any) => {
const modifiedDate = new Date(sheet.modifiedAt);
const daysDiff = (now.getTime() - modifiedDate.getTime()) / (1000 * 3600 * 24);
return daysDiff <= 7;
}).length;
const recentlyCreated = filteredSheets.filter((sheet: any) => {
const createdDate = new Date(sheet.createdAt);
const daysDiff = (now.getTime() - createdDate.getTime()) / (1000 * 3600 * 24);
return daysDiff <= 7;
}).length;
return {
success: true,
// Pagination info
pagination: {
page_number: sheetData.pageNumber,
page_size: sheetData.pageSize,
total_pages: sheetData.totalPages,
total_count: sheetData.totalCount,
filtered_count: filteredSheets.length,
},
// Main results
sheets: filteredSheets,
// Organized results
sheets_by_access_level: sheetsByAccess,
sheets_by_source_type: sheetsBySource,
// Summary statistics
summary: {
total_sheets: filteredSheets.length,
owned_sheets: (sheetsByAccess.OWNER || []).length,
admin_sheets: (sheetsByAccess.ADMIN || []).length,
editor_sheets: ((sheetsByAccess.EDITOR || []).length + (sheetsByAccess.EDITOR_SHARE || []).length),
commenter_sheets: (sheetsByAccess.COMMENTER || []).length,
viewer_sheets: (sheetsByAccess.VIEWER || []).length,
recently_modified: recentlyModified,
recently_created: recentlyCreated,
sheets_with_source: Object.values(sheetsBySource).flat().length,
},
// Access level breakdown
access_breakdown: Object.keys(sheetsByAccess).map(level => ({
access_level: level,
count: sheetsByAccess[level].length,
percentage: Math.round((sheetsByAccess[level].length / filteredSheets.length) * 100),
})),
// Applied filters info
filters_applied: {
name_filter: sheet_name || null,
exact_match: exact_match,
access_levels: access_level_filter || [],
modified_since: modified_since || null,
created_date_range: created_date_range || 'all',
sort_by: sort_by || 'name',
},
// API options used
api_options: {
access_api_level: access_api_level,
include_sheet_version: include_sheet_version,
include_source_info: include_source_info,
numeric_dates: numeric_dates,
},
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid request parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to access sheets listing');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to retrieve sheets: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,78 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import { smartsheetCommon, updateRowInSmartsheet } from '../common';
export const updateRow = createAction({
auth: smartsheetAuth,
name: 'update_row',
displayName: 'Update Row',
description: 'Updates an existing row.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
row_id: smartsheetCommon.row_id,
cells: smartsheetCommon.cells,
},
async run(context) {
const { sheet_id, row_id, cells } = context.propsValue;
const rowObj: any = {
id: row_id,
};
// Transform dynamic cells data into proper Smartsheet format
const cellsData = cells as Record<string, any>;
const transformedCells: any[] = [];
for (const [key, value] of Object.entries(cellsData)) {
if (value === undefined || value === null || value === '') {
continue; // Skip empty values
}
let columnId: number;
const cellObj: any = {};
if (key.startsWith('column_')) {
// Regular column value
columnId = parseInt(key.replace('column_', ''));
cellObj.columnId = columnId;
cellObj.value = value;
} else {
continue; // Skip unknown keys
}
transformedCells.push(cellObj);
}
// Only add cells array if we have cells to update
if (transformedCells.length > 0) {
rowObj.cells = transformedCells;
}
try {
const result = await updateRowInSmartsheet(context.auth.secret_text, sheet_id as string, [
[rowObj],
]);
return {
success: true,
row: result,
message: 'Row updated successfully',
cells_processed: transformedCells.length,
};
} catch (error: any) {
if (error.response?.status === 400) {
const errorBody = error.response.data;
throw new Error(`Bad Request: ${errorBody.message || 'Invalid row data or parameters'}`);
} else if (error.response?.status === 403) {
throw new Error('Insufficient permissions to update rows in this sheet');
} else if (error.response?.status === 404) {
throw new Error('Sheet not found or you do not have access to it');
} else if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
}
throw new Error(`Failed to update row: ${error.message}`);
}
},
});

View File

@@ -0,0 +1,900 @@
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpRequest, HttpMethod } from '@activepieces/pieces-common';
import crypto from 'crypto';
import { smartsheetAuth } from '../..';
export const smartsheetCommon = {
baseUrl: 'https://api.smartsheet.com/2.0',
sheet_id:(required=true)=> Property.Dropdown({
auth: smartsheetAuth,
displayName: 'Sheet',
description: 'Select a sheet',
required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first.',
options: [],
};
}
try {
const sheets = await listSheets(auth.secret_text);
if (sheets.length === 0) {
return {
disabled: true,
placeholder: 'No sheets found in your account.',
options: [],
};
}
return {
options: sheets.map((sheet: SmartsheetSheet) => ({
value: sheet.id.toString(),
label: sheet.name,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load sheets - check your connection.',
options: [],
};
}
},
}),
column_id: Property.Dropdown({
auth: smartsheetAuth,
displayName: 'Column',
description: 'Select a column',
required: true,
refreshers: ['sheet_id'],
options: async ({ auth, sheet_id }) => {
if (!auth) {
return {
disabled: true,
placeholder: '⚠️ Please authenticate with Smartsheet first',
options: [],
};
}
if (!sheet_id) {
return {
disabled: true,
placeholder: '📋 Please select a sheet first',
options: [],
};
}
try {
const columns = await getSheetColumns(
auth.secret_text
,
sheet_id as unknown as string,
);
if (columns.length === 0) {
return {
disabled: true,
placeholder: '📄 No columns found in this sheet',
options: [],
};
}
return {
options: columns.map((column: SmartsheetColumn) => ({
value: column.id.toString(),
label: column.title,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: '❌ Failed to load columns - check your permissions',
options: [],
};
}
},
}),
// Dynamic cell properties based on column types
cells: Property.DynamicProperties({
auth: smartsheetAuth,
displayName: 'Cells',
description: 'Cell data with properties based on column types',
required: true,
refreshers: ['sheet_id'],
props: async ({ auth, sheet_id }) => {
if (!auth || !sheet_id) return {};
const fields: DynamicPropsValue = {};
try {
const columns = await getSheetColumns(
auth.secret_text
,
sheet_id as unknown as string,
);
if (columns.length === 0) {
return {};
}
for (const column of columns) {
const baseProps = {
displayName: column.title,
required: false,
};
// Create cell properties based on column type
switch (column.type?.toLowerCase()) {
case 'TEXT_NUMBER':
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
});
break;
case 'DATE':
fields[`column_${column.id}`] = Property.DateTime({
...baseProps,
description: `Date/time value for ${column.title}`,
});
break;
case 'CHECKBOX':
fields[`column_${column.id}`] = Property.Checkbox({
...baseProps,
});
break;
case 'PICKLIST':
case 'MULTI_PICKLIST': {
if (column.options && column.options.length > 0) {
const dropdownOptions = column.options.map((option) => ({
label: option,
value: option,
}));
if (column.type?.toLowerCase() === 'multi_picklist') {
fields[`column_${column.id}`] = Property.StaticMultiSelectDropdown({
...baseProps,
description: `Multiple selection for ${column.title}`,
options: {
options: dropdownOptions,
},
});
} else {
fields[`column_${column.id}`] = Property.StaticDropdown({
...baseProps,
description: `Select option for ${column.title}`,
options: {
options: dropdownOptions,
},
});
}
} else {
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
description: `Value for ${column.title}`,
});
}
break;
}
case 'CONTACT_LIST':
case 'MULTI_CONTACT_LIST':
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
description: `Contact email(s) for ${column.title}. For multiple contacts, separate with commas.`,
});
break;
case 'DURATION':
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
description: `For example, 4d 6h 30m`,
});
break;
case 'PREDECESSOR':
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
description: `Predecessor row numbers for ${column.title}. Format: "1FS+2d,3SS" etc.`,
});
break;
case 'ABSTRACT_DATETIME':
fields[`column_${column.id}`] = Property.DateTime({
...baseProps,
description: `Date/time value for ${column.title}`,
});
break;
default:
fields[`column_${column.id}`] = Property.ShortText({
...baseProps,
description: `Value for ${column.title} (${column.type || 'unknown type'})`,
});
break;
}
}
return fields;
} catch (error) {
console.error('Failed to fetch columns for dynamic properties:', error);
return {};
}
},
}),
// Dynamic row selector
row_id: Property.Dropdown({
auth: smartsheetAuth,
displayName: 'Row',
required: true,
refreshers: ['sheet_id'],
options: async ({ auth, sheet_id }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first.',
options: [],
};
}
if (!sheet_id) {
return {
disabled: true,
placeholder: 'Please select a sheet first',
options: [],
};
}
try {
const sheet = await getSheet(auth.secret_text
, sheet_id as unknown as string);
const rows = sheet.rows || [];
if (rows.length === 0) {
return {
disabled: true,
placeholder: 'No rows found in this sheet',
options: [],
};
}
return {
disabled:false,
options: rows.slice(0, 100).map((row: any) => {
// Get the primary column value for display
const primaryCell = row.cells?.find((cell: any) =>
sheet.columns?.find((col: any) => col.id === cell.columnId && col.primary),
);
const displayValue =
primaryCell?.displayValue || primaryCell?.value || `Row ${row.rowNumber}`;
return {
value: row.id.toString(),
label: `${displayValue} (Row ${row.rowNumber})`,
};
}),
};
} catch (error) {
return {
disabled: true,
placeholder: 'Failed to load rows - check your permissions',
options: [],
};
}
},
}),
// Dynamic sheet selector for hyperlinks
hyperlink_sheet_id: Property.Dropdown({
auth: smartsheetAuth,
displayName: 'Target Sheet',
description: 'Select a sheet to link to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: '⚠️ Please authenticate with Smartsheet first',
options: [],
};
}
try {
const sheets = await listSheets(auth.secret_text
);
if (sheets.length === 0) {
return {
disabled: true,
placeholder: '📂 No sheets found in your account',
options: [],
};
}
return {
options: sheets.map((sheet: SmartsheetSheet) => ({
value: sheet.id.toString(),
label: sheet.name,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: '❌ Failed to load sheets - check your permissions',
options: [],
};
}
},
}),
// Dynamic report selector for hyperlinks
hyperlink_report_id: Property.Dropdown({
auth: smartsheetAuth,
displayName: 'Target Report',
description: 'Select a report to link to',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: '⚠️ Please authenticate with Smartsheet first',
options: [],
};
}
try {
const reports = await listReports(auth.secret_text
);
if (reports.length === 0) {
return {
disabled: true,
placeholder: '📊 No reports found in your account',
options: [],
};
}
return {
options: reports.map((report: SmartsheetReport) => ({
value: report.id.toString(),
label: `${report.name}${report.isSummaryReport ? ' (Summary)' : ' (Row Report)'}`,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: '❌ Failed to load reports - check your permissions',
options: [],
};
}
},
}),
// Dynamic column selector for search/filter operations
search_columns: Property.MultiSelectDropdown({
auth: smartsheetAuth,
displayName: 'Search Columns',
description: 'Select specific columns to search within (leave empty to search all columns)',
required: false,
refreshers: ['sheet_id'],
options: async ({ auth, sheet_id }) => {
if (!auth) {
return {
disabled: true,
placeholder: '⚠️ Please authenticate with Smartsheet first',
options: [],
};
}
if (!sheet_id) {
return {
disabled: true,
placeholder: '📋 Please select a sheet first',
options: [],
};
}
try {
const columns = await getSheetColumns(
auth.secret_text,
sheet_id as unknown as string,
);
const searchableColumns = columns.filter(
(column) => column.type?.toLowerCase() !== 'auto_number',
);
if (searchableColumns.length === 0) {
return {
disabled: true,
placeholder: '📄 No searchable columns found in this sheet',
options: [],
};
}
return {
options: searchableColumns.map((column: SmartsheetColumn) => ({
value: column.id.toString(),
label: `${column.title} (${column.type || 'unknown'})`,
})),
};
} catch (error) {
return {
disabled: true,
placeholder: '❌ Failed to load columns - check your permissions',
options: [],
};
}
},
}),
};
// Interfaces
export interface SmartsheetSheet {
id: number;
name: string;
accessLevel: string;
permalink: string;
createdAt: string;
modifiedAt: string;
}
export interface SmartsheetColumn {
id: number;
index: number;
title: string;
type?: string;
primary?: boolean;
options?: string[];
validation?: boolean;
width?: number;
hidden?: boolean;
locked?: boolean;
lockedForUser?: boolean;
}
export interface SmartsheetRow {
id: number;
rowNumber: number;
siblingId?: number;
expanded?: boolean;
createdAt: string;
modifiedAt: string;
cells: SmartsheetCell[];
}
export interface SmartsheetCell {
columnId: number;
value?: any;
displayValue?: string;
formula?: string;
}
export interface SmartsheetAttachment {
id: number;
name: string;
url: string;
attachmentType: string;
createdAt: string;
createdBy: {
name: string;
email: string;
};
}
export interface SmartsheetComment {
id: number;
text: string;
createdAt: string;
createdBy: {
name: string;
email: string;
};
}
export interface SmartsheetReport {
id: number;
name: string;
accessLevel: 'ADMIN' | 'COMMENTER' | 'EDITOR' | 'EDITOR_SHARE' | 'OWNER' | 'VIEWER';
isSummaryReport: boolean;
ownerId: number;
createdAt: string;
modifiedAt: string;
permalink: string;
owner?: string;
totalRowCount?: number;
version?: number;
}
export interface SmartsheetReportsResponse {
pageNumber: number;
pageSize: number | null;
totalPages: number;
totalCount: number;
data: SmartsheetReport[];
}
// Helper functions
export async function listSheets(accessToken: string): Promise<SmartsheetSheet[]> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest<{ data: SmartsheetSheet[] }>(request);
return response.body.data;
}
export async function getSheet(accessToken: string, sheetId: string): Promise<any> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest(request);
return response.body;
}
export async function getSheetColumns(
accessToken: string,
sheetId: string,
): Promise<SmartsheetColumn[]> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}/columns?include=columnType`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest<{ data: SmartsheetColumn[] }>(request);
return response.body.data;
}
export async function addRowToSmartsheet(
accessToken: string,
sheetId: string,
rowData: any,
queryParams: any = {},
): Promise<SmartsheetRow> {
// Build query string from parameters
const queryString = new URLSearchParams();
if (queryParams.allowPartialSuccess) {
queryString.append('allowPartialSuccess', 'true');
}
if (queryParams.overrideValidation) {
queryString.append('overrideValidation', 'true');
}
if (queryParams.accessApiLevel) {
queryString.append('accessApiLevel', queryParams.accessApiLevel.toString());
}
const url = `${smartsheetCommon.baseUrl}/sheets/${sheetId}/rows${
queryString.toString() ? '?' + queryString.toString() : ''
}`;
const request: HttpRequest = {
method: HttpMethod.POST,
url: url,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: rowData,
};
const response = await httpClient.sendRequest<{ result: SmartsheetRow[] }>(request);
return response.body.result[0];
}
export async function updateRowInSmartsheet(
accessToken: string,
sheetId: string,
rowData: any,
queryParams: any = {},
): Promise<SmartsheetRow> {
// Build query string from parameters
const queryString = new URLSearchParams();
if (queryParams.allowPartialSuccess) {
queryString.append('allowPartialSuccess', 'true');
}
if (queryParams.overrideValidation) {
queryString.append('overrideValidation', 'true');
}
if (queryParams.accessApiLevel) {
queryString.append('accessApiLevel', queryParams.accessApiLevel.toString());
}
const url = `${smartsheetCommon.baseUrl}/sheets/${sheetId}/rows${
queryString.toString() ? '?' + queryString.toString() : ''
}`;
const request: HttpRequest = {
method: HttpMethod.PUT,
url: url,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: rowData,
};
const response = await httpClient.sendRequest<{ result: SmartsheetRow[] }>(request);
return response.body.result[0];
}
export async function getRowAttachments(
accessToken: string,
sheetId: string,
rowId: string,
): Promise<SmartsheetAttachment[]> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}/rows/${rowId}/attachments`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest<{
data: SmartsheetAttachment[];
}>(request);
return response.body.data || [];
}
export async function findSheetsByName(
accessToken: string,
name: string,
): Promise<SmartsheetSheet[]> {
const sheets = await listSheets(accessToken);
return sheets.filter((sheet) => sheet.name.toLowerCase().includes(name.toLowerCase()));
}
export async function listReports(
accessToken: string,
modifiedSince?: string,
): Promise<SmartsheetReport[]> {
// Build query parameters
const queryParams: any = {};
if (modifiedSince) {
queryParams.modifiedSince = modifiedSince;
}
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/reports`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
};
const response = await httpClient.sendRequest<SmartsheetReportsResponse>(request);
return response.body.data || [];
}
// Webhook management functions
export interface SmartsheetWebhook {
id: number;
name: string;
callbackUrl: string;
scope: string;
scopeObjectId: number;
events: string[];
enabled: boolean;
status: string;
sharedSecret: string;
}
export async function subscribeWebhook(
accessToken: string,
webhookUrl: string,
sheetId: string,
webhookName: string,
): Promise<SmartsheetWebhook> {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${smartsheetCommon.baseUrl}/webhooks`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: {
name: webhookName,
callbackUrl: webhookUrl,
scope: 'sheet',
scopeObjectId: parseInt(sheetId),
events: ['*.*'],
version: 1,
},
};
const response = await httpClient.sendRequest<{ result: SmartsheetWebhook }>(request);
return response.body.result;
}
export async function enableWebhook(
accessToken: string,
webhookId: string,
): Promise<SmartsheetWebhook> {
const request: HttpRequest = {
method: HttpMethod.PUT,
url: `${smartsheetCommon.baseUrl}/webhooks/${webhookId}`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: {
enabled: true,
},
};
const response = await httpClient.sendRequest<{ result: SmartsheetWebhook }>(request);
return response.body.result;
}
export async function unsubscribeWebhook(accessToken: string, webhookId: string): Promise<void> {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${smartsheetCommon.baseUrl}/webhooks/${webhookId}`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
await httpClient.sendRequest(request);
}
export async function listWebhooks(accessToken: string): Promise<SmartsheetWebhook[]> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/webhooks`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
};
const response = await httpClient.sendRequest<{ data: SmartsheetWebhook[] }>(request);
return response.body.data || [];
}
export interface WebhookInformation {
webhookId: string;
sharedSecret: string;
webhookName: string;
}
export async function findOrCreateWebhook(
accessToken: string,
webhookUrl: string,
sheetId: string,
triggerIdentifier: string,
): Promise<SmartsheetWebhook> {
const webhookName = `AP-${triggerIdentifier.slice(-8)}-Sheet${sheetId}`;
const existingWebhooks = await listWebhooks(accessToken);
const existingWebhook = existingWebhooks.find(
(wh) => wh.callbackUrl === webhookUrl && wh.scopeObjectId.toString() === sheetId,
);
if (existingWebhook) {
if (existingWebhook.name !== webhookName) {
console.log(
`Found existing webhook ${existingWebhook.id} with different name: ${existingWebhook.name}. Expected: ${webhookName}`,
);
}
if (!existingWebhook.enabled || existingWebhook.status !== 'ENABLED') {
return await enableWebhook(accessToken, existingWebhook.id.toString());
}
return existingWebhook;
}
const newWebhook = await subscribeWebhook(accessToken, webhookUrl, sheetId, webhookName);
return await enableWebhook(accessToken, newWebhook.id.toString());
}
export function verifyWebhookSignature(
webhookSecret?: string,
webhookSignatureHeader?: string,
webhookRawBody?: any,
): boolean {
if (!webhookSecret || !webhookSignatureHeader || !webhookRawBody) {
return false;
}
try {
const hmac = crypto.createHmac('sha256', webhookSecret);
hmac.update(webhookRawBody);
const expectedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(webhookSignatureHeader, 'hex'),
Buffer.from(expectedSignature, 'hex'),
);
} catch (error) {
return false;
}
}
export async function getSheetRowDetails(
accessToken: string,
sheetId: string,
rowId: string,
): Promise<SmartsheetRow | null> {
try {
const req: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}/rows/${rowId}`,
headers: { Authorization: `Bearer ${accessToken}` },
};
const response = await httpClient.sendRequest<SmartsheetRow>(req);
return response.body;
} catch (e: any) {
if (e.response?.status === 404) {
console.log(`Row ${rowId} on sheet ${sheetId} not found during detail fetch.`);
return null;
}
console.error(`Error fetching row ${rowId} from sheet ${sheetId}:`, e);
throw e;
}
}
export async function getAttachmentFullDetails(accessToken: string, sheetId: string, attachmentId: string): Promise<SmartsheetAttachment | null> {
try {
const req: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}/attachments/${attachmentId}`,
headers: { 'Authorization': `Bearer ${accessToken}` }
};
const response = await httpClient.sendRequest<SmartsheetAttachment>(req);
return response.body;
} catch (e: any) {
if (e.response?.status === 404) {
console.log(`Attachment ${attachmentId} on sheet ${sheetId} not found.`);
return null;
}
console.error(`Error fetching attachment ${attachmentId} from sheet ${sheetId}:`, e);
throw e;
}
}
export async function getCommentFullDetails(accessToken: string, sheetId: string, discussionId: string, commentId: string): Promise<SmartsheetComment | null> {
try {
const req: HttpRequest = {
method: HttpMethod.GET,
url: `${smartsheetCommon.baseUrl}/sheets/${sheetId}/comments/${commentId}`,
headers: { 'Authorization': `Bearer ${accessToken}` }
};
const response = await httpClient.sendRequest<SmartsheetComment>(req);
return response.body;
} catch (e: any) {
if (e.response?.status === 404) {
console.log(`Comment ${commentId} in discussion ${discussionId} on sheet ${sheetId} not found.`);
return null;
}
console.error(`Error fetching comment ${commentId} from sheet ${sheetId}:`, e);
throw e;
}
}

View File

@@ -0,0 +1,139 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import {
smartsheetCommon,
findOrCreateWebhook,
WebhookInformation,
verifyWebhookSignature,
getAttachmentFullDetails,
} from '../common';
import { WebhookHandshakeStrategy } from '@activepieces/shared';
const TRIGGER_KEY = 'smartsheet_new_attachment_trigger';
export const newAttachmentTrigger = createTrigger({
auth: smartsheetAuth,
name: 'new_attachment_',
displayName: 'New Attachment Added',
description: 'Triggers when a new attachment is added to a row or sheet.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
sheetId: '12345',
eventType: 'created',
objectType: 'attachment',
id: 78901, // Attachment ID
parentId: 67890, // e.g., Row ID if attached to a row
parentType: 'ROW',
timestamp: '2023-10-28T12:10:00Z',
userId: 54321,
attachmentData: {
/* ... full attachment data ... */
},
},
handshakeConfiguration: {
strategy: WebhookHandshakeStrategy.BODY_PARAM_PRESENT,
paramName: 'challenge',
},
async onHandshake(context) {
return {
status: 200,
body: {
smartsheetHookResponse: (context.payload.body as any)['challenge'],
},
};
},
async onEnable(context) {
const { sheet_id } = context.propsValue;
if (!sheet_id) throw new Error('Sheet ID is required to enable the webhook.');
const triggerIdentifier = context.webhookUrl.substring(context.webhookUrl.lastIndexOf('/') + 1);
const webhook = await findOrCreateWebhook(
context.auth.secret_text,
context.webhookUrl,
sheet_id as string,
triggerIdentifier,
);
await context.store.put<WebhookInformation>(TRIGGER_KEY, {
webhookId: webhook.id.toString(),
sharedSecret: webhook.sharedSecret,
webhookName: webhook.name,
});
},
async onDisable(context) {
const { sheet_id } = context.propsValue;
if (!sheet_id) throw new Error('Sheet ID is required to enable the webhook.');
const triggerIdentifier = context.webhookUrl.substring(context.webhookUrl.lastIndexOf('/') + 1);
const webhook = await findOrCreateWebhook(
context.auth.secret_text,
context.webhookUrl,
sheet_id as string,
triggerIdentifier,
);
await context.store.put<WebhookInformation>(TRIGGER_KEY, {
webhookId: webhook.id.toString(),
sharedSecret: webhook.sharedSecret,
webhookName: webhook.name,
});
},
async run(context) {
const payload = context.payload.body as any;
const headers = context.payload.headers as Record<string, string | undefined>;
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (!webhookInfo) {
return [];
}
if (headers && headers['smartsheet-hook-challenge']) {
return [];
}
const webhookSecret = webhookInfo?.sharedSecret;
const webhookSignatureHeader = context.payload.headers['smartsheet-hmac-sha256'];
const rawBody = context.payload.rawBody;
if (!verifyWebhookSignature(webhookSecret, webhookSignatureHeader, rawBody)) {
return [];
}
if (payload.newWebhookStatus) {
return [];
}
if (!payload.events || !Array.isArray(payload.events) || payload.events.length === 0) {
return [];
}
const newAttachmentEvents = [];
for (const event of payload.events) {
if (event.objectType === 'attachment' && event.eventType === 'created') {
const eventOutput: any = { ...event, sheetId: payload.scopeObjectId?.toString() };
const objectSheetId = payload.scopeObjectId?.toString();
if (objectSheetId) {
try {
eventOutput.attachmentData = await getAttachmentFullDetails(
context.auth.secret_text,
objectSheetId,
event.id.toString(),
);
} catch (error: any) {
eventOutput.fetchError = error.message;
}
} else {
eventOutput.fetchError = 'scopeObjectId missing';
}
newAttachmentEvents.push(eventOutput);
}
}
return newAttachmentEvents;
},
});

View File

@@ -0,0 +1,138 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import {
smartsheetCommon,
unsubscribeWebhook,
WebhookInformation,
findOrCreateWebhook,
verifyWebhookSignature,
getCommentFullDetails,
} from '../common';
import { WebhookHandshakeStrategy } from '@activepieces/shared';
const TRIGGER_KEY = 'smartsheet_new_comment_trigger';
export const newCommentTrigger = createTrigger({
auth: smartsheetAuth,
name: 'new_comment_webhook',
displayName: 'New Comment Added',
description: 'Triggers when a new comment is added to a discussion on a sheet.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
sheetId: '12345',
eventType: 'created',
objectType: 'comment',
id: 89012, // Comment ID
discussionId: 45678,
parentId: 67890, // e.g., Row ID comment is on
parentType: 'ROW',
timestamp: '2023-10-28T12:15:00Z',
userId: 54321,
commentData: {
/* ... full comment data ... */
},
},
handshakeConfiguration: {
strategy: WebhookHandshakeStrategy.BODY_PARAM_PRESENT,
paramName: 'challenge',
},
async onHandshake(context) {
return {
status: 200,
body: {
smartsheetHookResponse: (context.payload.body as any)['challenge'],
},
};
},
async onEnable(context) {
const { sheet_id } = context.propsValue;
if (!sheet_id) throw new Error('Sheet ID is required to enable the webhook.');
const triggerIdentifier = context.webhookUrl.substring(context.webhookUrl.lastIndexOf('/') + 1);
const webhook = await findOrCreateWebhook(
context.auth.secret_text,
context.webhookUrl,
sheet_id as string,
triggerIdentifier,
);
await context.store.put<WebhookInformation>(TRIGGER_KEY, {
webhookId: webhook.id.toString(),
sharedSecret: webhook.sharedSecret,
webhookName: webhook.name,
});
},
async onDisable(context) {
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (webhookInfo && webhookInfo.webhookId) {
try {
await unsubscribeWebhook(context.auth.secret_text, webhookInfo.webhookId);
} catch (error: any) {
if (error.response?.status !== 404) {
console.error(`Error unsubscribing webhook ${webhookInfo.webhookId}: ${error.message}`);
}
}
await context.store.delete(TRIGGER_KEY);
}
},
async run(context): Promise<unknown[]> {
const payload = context.payload.body as any;
const headers = context.payload.headers as Record<string, string | undefined>;
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (!webhookInfo) {
return [];
}
if (headers && headers['smartsheet-hook-challenge']) {
return [];
}
const webhookSecret = webhookInfo?.sharedSecret;
const webhookSignatureHeader = context.payload.headers['smartsheet-hmac-sha256'];
const rawBody = context.payload.rawBody;
if (!verifyWebhookSignature(webhookSecret, webhookSignatureHeader, rawBody)) {
return [];
}
if (payload.newWebhookStatus) {
return [];
}
if (!payload.events || !Array.isArray(payload.events) || payload.events.length === 0) {
return [];
}
const newCommentEvents = [];
for (const event of payload.events) {
if (event.objectType === 'comment' && event.eventType === 'created') {
const eventOutput: any = { ...event, sheetId: payload.scopeObjectId?.toString() };
const objectSheetId = payload.scopeObjectId?.toString();
if (objectSheetId) {
try {
eventOutput.commentData = await getCommentFullDetails(
context.auth.secret_text,
objectSheetId,
event.discussionId.toString(),
event.id.toString(),
);
} catch (error: any) {
eventOutput.fetchError = error.message;
}
} else {
eventOutput.fetchError = 'scopeObjectId missing';
}
newCommentEvents.push(eventOutput);
}
}
return newCommentEvents;
},
});

View File

@@ -0,0 +1,145 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import {
smartsheetCommon,
unsubscribeWebhook,
getSheetRowDetails,
WebhookInformation,
findOrCreateWebhook,
verifyWebhookSignature,
} from '../common';
import { WebhookHandshakeStrategy } from '@activepieces/shared';
const TRIGGER_KEY = 'smartsheet_new_row_trigger';
export const newRowAddedTrigger = createTrigger({
auth: smartsheetAuth,
name: 'new_row_added',
displayName: 'New Row Added',
description: 'Triggers when a new row is added.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
sheetId: '12345',
eventType: 'created',
objectType: 'row',
id: 67890, // Row ID
timestamp: '2023-10-28T12:00:00Z',
userId: 54321,
rowData: {
id: 67890,
sheetId: 12345,
rowNumber: 15,
createdAt: '2023-10-28T12:00:00Z',
modifiedAt: '2023-10-28T12:00:00Z',
cells: [
{ columnId: 111, value: 'New Task A', displayValue: 'New Task A' },
{ columnId: 222, value: 'Pending', displayValue: 'Pending' },
],
},
},
handshakeConfiguration: {
strategy: WebhookHandshakeStrategy.BODY_PARAM_PRESENT,
paramName: 'challenge',
},
async onHandshake(context) {
return {
status: 200,
body: {
smartsheetHookResponse: (context.payload.body as any)['challenge'],
},
};
},
async onEnable(context) {
const { sheet_id } = context.propsValue;
if (!sheet_id) throw new Error('Sheet ID is required to enable the webhook.');
const triggerIdentifier = context.webhookUrl.substring(context.webhookUrl.lastIndexOf('/') + 1);
const webhook = await findOrCreateWebhook(
context.auth.secret_text,
context.webhookUrl,
sheet_id as string,
triggerIdentifier,
);
await context.store.put<WebhookInformation>(TRIGGER_KEY, {
webhookId: webhook.id.toString(),
sharedSecret: webhook.sharedSecret,
webhookName: webhook.name,
});
},
async onDisable(context) {
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (webhookInfo && webhookInfo.webhookId) {
try {
await unsubscribeWebhook(context.auth.secret_text, webhookInfo.webhookId);
} catch (error: any) {
if (error.response?.status !== 404) {
console.error(`Error unsubscribing webhook ${webhookInfo.webhookId}: ${error.message}`);
}
}
await context.store.delete(TRIGGER_KEY);
}
},
async run(context) {
const payload = context.payload.body as any;
const headers = context.payload.headers as Record<string, string | undefined>;
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (!webhookInfo) {
return [];
}
if (headers && headers['smartsheet-hook-challenge']) {
return [];
}
const webhookSecret = webhookInfo?.sharedSecret;
const webhookSignatureHeader = context.payload.headers['smartsheet-hmac-sha256'];
const rawBody = context.payload.rawBody;
if (!verifyWebhookSignature(webhookSecret, webhookSignatureHeader, rawBody)) {
return [];
}
if (payload.newWebhookStatus) {
return [];
}
if (!payload.events || !Array.isArray(payload.events) || payload.events.length === 0) {
return [];
}
const newRowEvents = [];
for (const event of payload.events) {
if (event.objectType === 'row' && event.eventType === 'created') {
const eventOutput: any = { ...event, sheetId: payload.scopeObjectId?.toString() };
const objectSheetId = payload.scopeObjectId?.toString();
if (objectSheetId) {
try {
eventOutput.rowData = await getSheetRowDetails(
context.auth.secret_text,
objectSheetId,
event.id.toString(),
);
} catch (error: any) {
console.warn(
`Failed to fetch full details for new row ID ${event.id}: ${error.message}`,
);
eventOutput.fetchError = error.message;
}
} else {
eventOutput.fetchError = 'scopeObjectId missing, cannot fetch row details.';
}
newRowEvents.push(eventOutput);
}
}
return newRowEvents;
},
});

View File

@@ -0,0 +1,134 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { smartsheetAuth } from '../../index';
import {
smartsheetCommon,
findOrCreateWebhook,
WebhookInformation,
getSheetRowDetails,
verifyWebhookSignature,
unsubscribeWebhook,
} from '../common';
import { WebhookHandshakeStrategy } from '@activepieces/shared';
const TRIGGER_KEY = 'smartsheet_updated_row_trigger';
export const updatedRowTrigger = createTrigger({
auth: smartsheetAuth,
name: 'updated_row',
displayName: 'Row Updated',
description: 'Triggers when an existing row is updated.',
props: {
sheet_id: smartsheetCommon.sheet_id(),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
sheetId: '12345',
eventType: 'updated',
objectType: 'row',
id: 67890,
columnId: 333,
timestamp: '2023-10-28T12:05:00Z',
userId: 54321,
rowData: {
/* ... full row data ... */
},
},
handshakeConfiguration: {
strategy: WebhookHandshakeStrategy.BODY_PARAM_PRESENT,
paramName: 'challenge',
},
async onHandshake(context) {
return {
status: 200,
body: {
smartsheetHookResponse: (context.payload.body as any)['challenge'],
},
};
},
async onEnable(context) {
const { sheet_id } = context.propsValue;
if (!sheet_id) throw new Error('Sheet ID is required.');
const triggerIdentifier = context.webhookUrl.substring(context.webhookUrl.lastIndexOf('/') + 1);
const webhook = await findOrCreateWebhook(
context.auth.secret_text,
context.webhookUrl,
sheet_id as string,
triggerIdentifier,
);
await context.store.put<WebhookInformation>(TRIGGER_KEY, {
webhookId: webhook.id.toString(),
sharedSecret: webhook.sharedSecret,
webhookName: webhook.name,
});
},
async onDisable(context) {
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (webhookInfo && webhookInfo.webhookId) {
try {
await unsubscribeWebhook(context.auth.secret_text, webhookInfo.webhookId);
} catch (error: any) {
if (error.response?.status !== 404) {
console.error(`Error unsubscribing webhook ${webhookInfo.webhookId}: ${error.message}`);
}
}
await context.store.delete(TRIGGER_KEY);
}
},
async run(context): Promise<unknown[]> {
const payload = context.payload.body as any;
const headers = context.payload.headers as Record<string, string | undefined>;
const webhookInfo = await context.store.get<WebhookInformation>(TRIGGER_KEY);
if (!webhookInfo) {
return [];
}
if (headers && headers['smartsheet-hook-challenge']) {
return [];
}
const webhookSecret = webhookInfo?.sharedSecret;
const webhookSignatureHeader = context.payload.headers['smartsheet-hmac-sha256'];
const rawBody = context.payload.rawBody;
if (!verifyWebhookSignature(webhookSecret, webhookSignatureHeader, rawBody)) {
return [];
}
if (payload.newWebhookStatus) {
return [];
}
if (!payload.events || !Array.isArray(payload.events) || payload.events.length === 0) {
return [];
}
const updatedRowEvents = [];
for (const event of payload.events) {
if (event.objectType === 'row' && event.eventType === 'updated') {
const eventOutput: any = { ...event, sheetId: payload.scopeObjectId?.toString() };
const objectSheetId = payload.scopeObjectId?.toString();
if (objectSheetId) {
try {
eventOutput.rowData = await getSheetRowDetails(
context.auth.secret_text,
objectSheetId,
event.id.toString(),
);
} catch (error: any) {
eventOutput.fetchError = error.message;
}
} else {
eventOutput.fetchError = 'scopeObjectId missing';
}
updatedRowEvents.push(eventOutput);
}
}
return updatedRowEvents;
},
});