chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
import { IntegrationDefinitionProps, z } from '@botpress/sdk'
|
||||
|
||||
const fieldTypeSchema = z.enum([
|
||||
'Color',
|
||||
'DateTime',
|
||||
'Email',
|
||||
'ExtFileRef',
|
||||
'File',
|
||||
'Image',
|
||||
'Link',
|
||||
'Multimage',
|
||||
'MultiReference',
|
||||
'Number',
|
||||
'Option',
|
||||
'Phone',
|
||||
'PlainText',
|
||||
'Reference',
|
||||
'RichText',
|
||||
'Switch',
|
||||
'VideoLink',
|
||||
])
|
||||
|
||||
const collectionSchema = z.object({
|
||||
id: z.string().optional().describe('Unique identifier for a Collection').title('Collection ID'),
|
||||
displayName: z.string().describe('Name given to the Collection').title('Collection Name'),
|
||||
singularName: z
|
||||
.string()
|
||||
.describe('The name of one Item in Collection (e.g. "Blog Post" if the Collection is called "Blog Posts")')
|
||||
.title('Collection Singular Name'),
|
||||
slug: z.string().optional().describe('Slug of Collection in Site URL structure').title('Collection Slug'),
|
||||
createdOn: z.string().optional().describe('The date the collection was created').title('Collection Created On'),
|
||||
lastUpdated: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The date the collection was last updated')
|
||||
.title('Collection Last Updated'),
|
||||
})
|
||||
|
||||
const collectionDetailsSchema = collectionSchema.extend({
|
||||
fields: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().describe('Unique identifier for a Field').title('Field ID'),
|
||||
isRequired: z.boolean().describe('define whether a field is required in a collection').title('Is Required'),
|
||||
type: fieldTypeSchema
|
||||
.describe('Choose these appropriate field type for your collection data')
|
||||
.title('Field Type'),
|
||||
displayName: z.string().describe('The name of the Field').title('Field Name'),
|
||||
isEditable: z.boolean().nullable().describe('Define whether the field is editable').title('Is Editable'),
|
||||
slug: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe(
|
||||
'Slug of Field in Site URL structure. Slugs should be all lowercase with no spaces. Any spaces will be converted to "-".'
|
||||
)
|
||||
.title('Field Slug'),
|
||||
helpText: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('Additional text to help anyone filling out this field')
|
||||
.title('Field Help Text'),
|
||||
validation: z.any().describe('The validation for the field').title('Field Validation'),
|
||||
})
|
||||
)
|
||||
.title('Fields')
|
||||
.describe('Array of fields in the collection'),
|
||||
})
|
||||
|
||||
const itemSchemaInput = z.object({
|
||||
id: z.string().optional().describe('Unique identifier for the Item').title('Item ID'),
|
||||
fieldData: z
|
||||
.object({
|
||||
name: z.string().min(1, 'Field name is required').describe('Name of the Item').title('Item Name'),
|
||||
slug: z
|
||||
.string()
|
||||
.describe(
|
||||
'URL structure of the Item in your site. Note: Updates to an item slug will break all links referencing the old slug.'
|
||||
)
|
||||
.title('Item Slug'),
|
||||
})
|
||||
.describe('The field data of your Webflow item')
|
||||
.title('Field Data'),
|
||||
cmsLocaleId: z.string().optional().describe('Identifier for the locale of the CMS item').title('CMS Locale ID'),
|
||||
isArchived: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Boolean determining if the Item is set to archived')
|
||||
.title('Is Archived'),
|
||||
isDraft: z.boolean().optional().describe('Boolean determining if the Item is set to draft').title('Is Draft'),
|
||||
})
|
||||
|
||||
const itemSchemaOutput = itemSchemaInput.extend({
|
||||
lastPublished: z.string().nullable().describe('The date the item was last published').title('Last Published'),
|
||||
lastUpdated: z.string().optional().describe('The date the item was last updated').title('Last Updated'),
|
||||
createdOn: z.string().optional().describe('The date the item was created').title('Created On'),
|
||||
})
|
||||
|
||||
const paginationSchema = z.object({
|
||||
limit: z.number().default(100).optional().describe('The number of items to return').title('Limit'),
|
||||
offset: z.number().default(0).optional().describe('The number of items to skip').title('Offset'),
|
||||
})
|
||||
|
||||
export const actions = {
|
||||
listCollections: {
|
||||
title: 'List Collections',
|
||||
description: 'Retrieve a list of all collections in your Webflow site.',
|
||||
input: {
|
||||
schema: z.object({}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
collections: z.array(collectionSchema).describe('Array of collections').title('Collections'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
getCollectionDetails: {
|
||||
title: 'Get Collection Details',
|
||||
description: 'Retrieve detailed information about a specific collection in your Webflow site.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
collectionDetails: collectionDetailsSchema.describe('Details of the collection').title('Collection Details'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createCollection: {
|
||||
title: 'Create Collection',
|
||||
description: 'Create a new collection in your Webflow site.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionInfo: collectionSchema.describe('Informations of the collection to create.').title('Collection Info'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
collectionDetails: collectionDetailsSchema
|
||||
.describe('Details of the new collection')
|
||||
.title('Collection Details'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
deleteCollection: {
|
||||
title: 'Delete Collection',
|
||||
description: 'Delete a specific collection from your Webflow site.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({}),
|
||||
},
|
||||
},
|
||||
listItems: {
|
||||
title: 'List Collection Items',
|
||||
description:
|
||||
'List items in a Webflow collection. By default, this lists draft items. To list live items, set the "Is Live Items" parameter to true.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
pagination: paginationSchema.optional().describe('Pagination parameters').title('Pagination'),
|
||||
isLiveItems: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('checkbox to decide if the list is for live items or not')
|
||||
.title('Is Live Items'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
items: z.array(itemSchemaOutput).describe('Array of items').title('Items'),
|
||||
pagination: paginationSchema
|
||||
.extend({
|
||||
total: z.number().min(0).optional().describe('Total number of items in the collection').title('Total'),
|
||||
})
|
||||
.describe('Pagination details')
|
||||
.title('Pagination'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
getItem: {
|
||||
title: 'Get Collection Item',
|
||||
description: 'Retrieve a specific item from a Webflow collection using its unique ID.',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
isLiveItems: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('checkbox to decide if the list is for live items or not')
|
||||
.title('Is Live Items'),
|
||||
itemID: z.string().min(1, 'Item ID is required').describe('The ID of your Webflow item').title('Item ID'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
itemDetails: itemSchemaOutput.describe('Details of the item').title('Item Details'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
createItems: {
|
||||
title: 'Create Collection item(s)',
|
||||
description: 'Create one or multiple items in a Webflow collection',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
isLiveItems: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('checkbox to decide if the list is for live items or not')
|
||||
.title('Is Live Items'),
|
||||
items: z.array(itemSchemaInput).describe('Items to add to the collection').title('Items'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
items: z.array(itemSchemaOutput).describe('Details of the items created').title('Items'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
updateItems: {
|
||||
title: 'Update Item(s)',
|
||||
description: 'Update one or more items in a collection',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
items: z.array(itemSchemaInput).describe('Array of items to update').title('Items'),
|
||||
isLiveItems: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('checkbox to decide if the list is for live items or not')
|
||||
.title('Is Live Items'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
items: z.array(itemSchemaOutput).describe('Array of updated collection items').title('Items'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
deleteItems: {
|
||||
title: 'Delete Item(s)',
|
||||
description: 'Delete one or more items from a collection',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
itemIDs: z
|
||||
.object({
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z
|
||||
.string()
|
||||
.min(1, 'Item ID is required')
|
||||
.describe('Unique identifier for the Item')
|
||||
.title('Item ID'),
|
||||
})
|
||||
)
|
||||
.title('Items')
|
||||
.describe('Array of items to delete'),
|
||||
})
|
||||
.describe('Array of item IDs to delete')
|
||||
.title('Item IDs'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({}),
|
||||
},
|
||||
},
|
||||
publishItems: {
|
||||
title: 'Publish Item(s)',
|
||||
description: 'Publish one or more items in a collection',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
itemIds: z
|
||||
.array(z.string().min(1, 'Item ID is required').describe('Unique identifier for the Item').title('Item ID'))
|
||||
.describe('Array of item IDs to publish')
|
||||
.title('Item IDs'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({
|
||||
publishedItemIds: z.array(z.string()).describe('Array of published item IDs').title('Published Item IDs'),
|
||||
errors: z.array(z.string()).describe('Array of errors encountered during publishing').title('Errors'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
unpublishLiveItems: {
|
||||
title: 'Unpublish Live Item(s)',
|
||||
description: 'Unpublish one or more live items in a collection',
|
||||
input: {
|
||||
schema: z.object({
|
||||
collectionID: z
|
||||
.string()
|
||||
.min(1, 'Collection ID is required')
|
||||
.describe('The ID of your Webflow collection')
|
||||
.title('Collection ID'),
|
||||
itemIds: z
|
||||
.array(z.string().min(1, 'Item ID is required').describe('Unique identifier for the Item').title('Item ID'))
|
||||
.describe('Array of item IDs to unpublish')
|
||||
.title('Item IDs'),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
schema: z.object({}),
|
||||
},
|
||||
},
|
||||
} satisfies IntegrationDefinitionProps['actions']
|
||||
@@ -0,0 +1,62 @@
|
||||
import { IntegrationDefinitionProps } from '@botpress/sdk'
|
||||
import { formSchema, siteSchema, pageSchema, itemSchema, commentSchema } from './sub-schemas'
|
||||
|
||||
export const events = {
|
||||
formSubmission: {
|
||||
title: 'Form Submission',
|
||||
description: 'Information about a form that was submitted',
|
||||
schema: formSchema,
|
||||
},
|
||||
sitePublish: {
|
||||
title: 'Site Publish',
|
||||
description: 'Information about a site that was published',
|
||||
schema: siteSchema,
|
||||
},
|
||||
pageCreated: {
|
||||
title: 'Page Created',
|
||||
description: 'Information about a new pages',
|
||||
schema: pageSchema,
|
||||
},
|
||||
pageMetadataUpdated: {
|
||||
title: 'Page Metadata Updated',
|
||||
description: "Information about a page's updated metadata and/or settings",
|
||||
schema: pageSchema,
|
||||
},
|
||||
pageDeleted: {
|
||||
title: 'Page Deleted',
|
||||
description: 'Information about a page that was deleted',
|
||||
schema: pageSchema,
|
||||
},
|
||||
collectionItemCreated: {
|
||||
title: 'Collection Item Created',
|
||||
description: 'Information about a new collection item',
|
||||
schema: itemSchema,
|
||||
},
|
||||
collectionItemDeleted: {
|
||||
title: 'Collection Item Deleted',
|
||||
description: 'Information about a deleted collection item',
|
||||
schema: itemSchema,
|
||||
},
|
||||
collectionItemUpdated: {
|
||||
title: 'Collection Item Updated',
|
||||
description: 'Information about an updated collection item',
|
||||
schema: itemSchema,
|
||||
},
|
||||
collectionItemPublished: {
|
||||
title: 'Collection Item Published',
|
||||
description: 'Information about a collection item that was published',
|
||||
schema: itemSchema,
|
||||
},
|
||||
collectionItemUnpublished: {
|
||||
title: 'Collection Item Unpublished',
|
||||
description: 'Information about a collection item that was removed from the live site',
|
||||
schema: itemSchema,
|
||||
},
|
||||
// user not supported
|
||||
// ecomm not supported
|
||||
commentCreated: {
|
||||
title: 'New Comment Thread',
|
||||
description: 'Information about a new comment thread or reply',
|
||||
schema: commentSchema,
|
||||
},
|
||||
} satisfies IntegrationDefinitionProps['events']
|
||||
@@ -0,0 +1,113 @@
|
||||
import { z } from '@botpress/sdk'
|
||||
|
||||
export const formSchema = z.object({
|
||||
name: z.string().optional().describe('The name of the form').title('Form Name'),
|
||||
siteId: z.string().describe('The ID of the site that the form was submitted from').title('Site ID'),
|
||||
data: z.record(z.string()).optional().describe('The data submitted in the form').title('Form Data'),
|
||||
schema: z
|
||||
.array(
|
||||
z.object({
|
||||
fieldName: z.string().optional().describe('Form field name').title('Field Name'),
|
||||
fieldType: z
|
||||
.enum(['FormTextInput', 'FormTextarea', 'FormCheckboxInput', 'FormRadioInput', 'FormFileUploadInput'])
|
||||
.optional()
|
||||
.describe('Form field type')
|
||||
.title('Field Type'),
|
||||
fieldElementId: z.string().describe('Element ID of the Form Field').title('Field Element ID'),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe('A list of fields from the submitted form')
|
||||
.title('Form Schema'),
|
||||
submittedAt: z.string().optional().describe('The timestamp the form was submitted').title('Submitted At'),
|
||||
id: z.string().describe('the ID of the event').title('Form Submission ID'),
|
||||
formId: z.string().describe('The ID of the form submission').title('Form ID'),
|
||||
formElementId: z.string().describe('The uniqueID of the Form element').title('Form Element ID'),
|
||||
})
|
||||
|
||||
export const siteSchema = z.object({
|
||||
domain: z.array(z.string()).optional().describe('The domains that were published').title('Site Domain'),
|
||||
site: z.string().optional().describe('The ID of the site that was published').title('Site Name'),
|
||||
publishedOn: z.string().describe('Timestamp when the site was published').title('Published On'),
|
||||
publishedBy: z
|
||||
.object({
|
||||
displayName: z.string().describe('The name andID of the user who published the site').title('Published By'),
|
||||
})
|
||||
.describe('Information about the user who published the site')
|
||||
.title('Published By'),
|
||||
})
|
||||
|
||||
export const pageSchema = z.object({
|
||||
siteId: z.string().describe('ID of the site').title('Site ID'),
|
||||
pageId: z.string().describe('ID of the page').title('Page ID'),
|
||||
pageTitle: z.string().optional().describe('Title of the page').title('Page Title'),
|
||||
createdOn: z.string().optional().describe('Timestamp when the page was created').title('Created On'),
|
||||
lastUpdated: z.string().optional().describe('Timestamp when the page was last updated').title('Last Updated'),
|
||||
deletedOn: z.string().optional().describe('Timestamp when the page was deleted').title('Deleted On'),
|
||||
publishedPath: z.string().optional().describe('Published path of the page').title('Published Path'),
|
||||
})
|
||||
|
||||
export const itemSchema = z.object({
|
||||
id: z.string().describe('Unique identifier for the Item').title('Item ID'),
|
||||
workspaceId: z.string().describe('Unique identifier of the workspace').title('Workspace ID'),
|
||||
siteId: z.string().describe('Unique identifier of the site').title('Site ID'),
|
||||
collectionId: z.string().describe('Unique identifier of the collection').title('Collection ID'),
|
||||
fieldData: z
|
||||
.object({
|
||||
name: z.string().describe('Name of the item').title('Item Name'),
|
||||
slug: z.string().describe('Slug of the item').title('Item Slug'),
|
||||
})
|
||||
.describe('Field data of the item')
|
||||
.title('Field Data'),
|
||||
lastPublished: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('Timestamp when the item was last published')
|
||||
.title('Last Published'),
|
||||
lastUpdated: z.string().optional().describe('Timestamp when the item was last updated').title('Last Updated'),
|
||||
createdOn: z.string().optional().describe('Timestamp when the item was created').title('Created On'),
|
||||
isArchived: z.boolean().optional().describe('Whether the item is archived').title('Is Archived'),
|
||||
isDraft: z.boolean().optional().describe('Whether the item is a draft').title('Is Draft'),
|
||||
cmsLocaleId: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('Unique identifier of the CMS locale for this item')
|
||||
.title('CMS Locale ID'),
|
||||
})
|
||||
|
||||
export const commentSchema = z.object({
|
||||
threadId: z.string().describe('Unique identifier for the comment thread').title('Thread ID'),
|
||||
commentId: z.string().describe('Unique identifier for the comment reply').title('Comment ID'),
|
||||
type: z.string().describe('The type of comment payload').title('Comment Type'),
|
||||
siteId: z.string().describe('The site unique identifier').title('Site ID'),
|
||||
pageId: z.string().describe('The page unique identifier').title('Page ID'),
|
||||
localeId: z.string().describe('The locale unique identifier').title('Locale ID'),
|
||||
breakpoint: z.string().describe('The breakpoint the comment was left on').title('Breakpoint'),
|
||||
url: z.string().describe('The URL of the page the comment was left on').title('Comment URL'),
|
||||
content: z.string().describe('The content of the comment reply').title('Comment Content'),
|
||||
isResolved: z.boolean().describe('Boolean determining if the comment thread is resolved').title('Is Resolved'),
|
||||
author: z
|
||||
.object({
|
||||
userId: z.string().describe('The unique identifier of the author').title('Author ID'),
|
||||
email: z.string().describe('Email of the author').title('Author Email'),
|
||||
name: z.string().describe('Name of the author').title('Author Name'),
|
||||
})
|
||||
.describe('Information about the author of the comment')
|
||||
.title('Author'),
|
||||
mentionedUsers: z
|
||||
.array(
|
||||
z.object({
|
||||
userId: z.string().describe('The unique identifier of the mentioned user').title('Mentioned User ID'),
|
||||
email: z.string().describe('Email of the user').title('Mentioned User Email'),
|
||||
name: z.string().describe('Name of the user').title('Mentioned User Name'),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'List of mentioned users. This is an empty array until email notifications are sent, which can take up to 5 minutes after the comment is created.'
|
||||
)
|
||||
.title('Mentioned Users'),
|
||||
createdOn: z.string().describe('The date the item was created').title('Created On'),
|
||||
lastUpdated: z.string().describe('The date the item was last updated').title('Last Updated'),
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import rootConfig from '../../eslint.config.mjs'
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
# Webflow integration
|
||||
|
||||
## Description
|
||||
|
||||
This integration allows Botpress bots to directly interact with Webflow’s CMS collections through full CRUD (Create, Read, Update, Delete) operations. By connecting your bot to Webflow, you can dynamically manage and display content—such as blog posts, products, or user submissions—without leaving the conversation. Whether it’s retrieving live data, adding new entries, or updating existing ones, this integration turns your chatbot into a real-time content manager for Webflow-powered sites.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Configuration
|
||||
|
||||
You can create a token for you site using this [documentation](https://developers.webflow.com/data/reference/authentication/site-token)
|
||||
your siteId can be found in the settings of the site you are working on
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="1080" height="1080" viewBox="0 0 1080 1080" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1080" height="1080" rx="540" fill="#146EF5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M902.062 348.75L687.217 768.75H485.417L575.329 594.684H571.295C497.118 690.976 386.444 754.365 228.75 768.75V597.093C228.75 597.093 329.63 591.134 388.935 528.784H228.75V348.753H408.781V496.826L412.822 496.809L486.389 348.753H622.541V495.887L626.582 495.881L702.909 348.75H902.062Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1,58 @@
|
||||
import { z, IntegrationDefinition } from '@botpress/sdk'
|
||||
import { events } from 'definitions/events'
|
||||
import { actions } from './definitions/actions'
|
||||
|
||||
export default new IntegrationDefinition({
|
||||
name: 'webflow',
|
||||
version: '3.1.4',
|
||||
title: 'Webflow',
|
||||
description: 'CRUD operations for Webflow CMS',
|
||||
readme: 'hub.md',
|
||||
icon: 'icon.svg',
|
||||
configuration: {
|
||||
schema: z.object({
|
||||
apiToken: z.string().min(1, 'API Token is required').describe('Your Webflow API Token').title('API Token'),
|
||||
siteID: z.string().min(1, 'Site ID is required').describe('The ID of your Webflow site').title('Site ID'),
|
||||
}),
|
||||
},
|
||||
actions: {
|
||||
listCollections: actions.listCollections,
|
||||
getCollectionDetails: actions.getCollectionDetails,
|
||||
createCollection: actions.createCollection,
|
||||
deleteCollection: actions.deleteCollection,
|
||||
|
||||
listItems: actions.listItems,
|
||||
getItem: actions.getItem,
|
||||
createItems: actions.createItems,
|
||||
updateItems: actions.updateItems,
|
||||
deleteItems: actions.deleteItems,
|
||||
publishItems: actions.publishItems,
|
||||
unpublishLiveItems: actions.unpublishLiveItems,
|
||||
},
|
||||
events: {
|
||||
formSubmission: events.formSubmission,
|
||||
|
||||
sitePublish: events.sitePublish,
|
||||
|
||||
pageCreated: events.pageCreated,
|
||||
pageMetadataUpdated: events.pageMetadataUpdated,
|
||||
pageDeleted: events.pageDeleted,
|
||||
|
||||
collectionItemCreated: events.collectionItemCreated,
|
||||
collectionItemDeleted: events.collectionItemDeleted,
|
||||
collectionItemUpdated: events.collectionItemUpdated,
|
||||
collectionItemPublished: events.collectionItemPublished,
|
||||
collectionItemUnpublished: events.collectionItemUnpublished,
|
||||
|
||||
// user not supported
|
||||
// ecomm not supported
|
||||
commentCreated: events.commentCreated,
|
||||
},
|
||||
__advanced: {
|
||||
useLegacyZuiTransformer: true,
|
||||
},
|
||||
attributes: {
|
||||
category: 'Developer Tools',
|
||||
repo: 'botpress',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@botpresshub/webflow",
|
||||
"scripts": {
|
||||
"build": "bp add -y && bp build",
|
||||
"check:type": "tsc --noEmit",
|
||||
"check:bplint": "bp lint",
|
||||
"test": "vitest --run"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@botpress/client": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*",
|
||||
"axios": "^1.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@botpress/cli": "workspace:*",
|
||||
"@botpress/sdk": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export class WebflowClient {
|
||||
private _axiosClient: AxiosInstance
|
||||
|
||||
public constructor(token: string) {
|
||||
this._axiosClient = axios.create({
|
||||
baseURL: 'https://api.webflow.com/v2',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public listItems = async (
|
||||
collectionID: string,
|
||||
offset: number,
|
||||
limit: number,
|
||||
isLiveItems?: boolean
|
||||
): Promise<bp.actions.listItems.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items${isLiveItems ? '/live' : ''}?offset=${offset}&limit=${limit}`
|
||||
const resp = await this._axiosClient.get<bp.actions.listItems.output.Output>(path)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
public getItem = async (
|
||||
collectionID: string,
|
||||
itemID: string,
|
||||
isLiveItems?: boolean
|
||||
): Promise<bp.actions.getItem.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items/${itemID}${isLiveItems ? '/live' : ''}`
|
||||
const resp = await this._axiosClient.get(path)
|
||||
return { itemDetails: resp.data }
|
||||
}
|
||||
|
||||
public createItems = async (
|
||||
collectionID: string,
|
||||
items: object,
|
||||
isLiveItems?: boolean
|
||||
): Promise<bp.actions.createItems.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items${isLiveItems ? '/live' : ''}?skipInvalidFiles=true`
|
||||
const resp = await this._axiosClient.post(path, { items })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
public updateItem = async (
|
||||
collectionID: string,
|
||||
items: object,
|
||||
isLiveItems?: boolean
|
||||
): Promise<bp.actions.updateItems.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items${isLiveItems ? '/live' : ''}?skipInvalidFiles=true`
|
||||
const resp = await this._axiosClient.patch(path, { items })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
public deleteItem = async (collectionID: string, itemIds: object): Promise<bp.actions.deleteItems.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items`
|
||||
await this._axiosClient.delete(path, { data: itemIds })
|
||||
return {}
|
||||
}
|
||||
|
||||
public publishItems = async (
|
||||
collectionID: string,
|
||||
itemIds: object
|
||||
): Promise<bp.actions.publishItems.output.Output> => {
|
||||
const path = `/collections/${collectionID}/items/publish`
|
||||
const resp = await this._axiosClient.post(path, { itemIds })
|
||||
return resp.data
|
||||
}
|
||||
|
||||
public unpublishLiveItems = async (
|
||||
collectionID: string,
|
||||
itemIds: object
|
||||
): Promise<bp.actions.unpublishLiveItems.output.Output> => {
|
||||
const path = `https://api.webflow.com/v2/collections/${collectionID}/items/publish`
|
||||
await this._axiosClient.post(path, { items: itemIds })
|
||||
return {}
|
||||
}
|
||||
|
||||
public listCollections = async (siteID: string): Promise<bp.actions.listCollections.output.Output> => {
|
||||
const path = `/sites/${siteID}/collections`
|
||||
const resp = await this._axiosClient.get(path)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
public getCollectionDetails = async (
|
||||
collectionID: string
|
||||
): Promise<bp.actions.getCollectionDetails.output.Output> => {
|
||||
const path = `/collections/${collectionID}`
|
||||
const resp = await this._axiosClient.get(path)
|
||||
return { collectionDetails: resp.data }
|
||||
}
|
||||
|
||||
public createCollection = async (
|
||||
siteID: string,
|
||||
collectionInfo: object
|
||||
): Promise<bp.actions.createCollection.output.Output> => {
|
||||
const path = `/sites/${siteID}/collections`
|
||||
const resp = await this._axiosClient.post(path, collectionInfo)
|
||||
return { collectionDetails: resp.data }
|
||||
}
|
||||
|
||||
public deleteCollection = async (collectionID: string): Promise<bp.actions.deleteCollection.output.Output> => {
|
||||
const path = `/collections/${collectionID}`
|
||||
await this._axiosClient.delete(path)
|
||||
return {}
|
||||
}
|
||||
public listWebhooks = async (siteID: string): Promise<{ triggerType: string; id: string }[]> => {
|
||||
const path = `/sites/${siteID}/webhooks`
|
||||
const { data } = await this._axiosClient.get<{ webhooks: { triggerType: string; id: string }[] }>(path)
|
||||
return data.webhooks
|
||||
}
|
||||
|
||||
public createWebhook = async (triggerType: string, siteID: string, webhookUrl: string): Promise<void> => {
|
||||
const path = `/sites/${siteID}/webhooks`
|
||||
await this._axiosClient.post(path, {
|
||||
triggerType,
|
||||
url: webhookUrl,
|
||||
})
|
||||
}
|
||||
|
||||
public deleteWebhooks = async (webhookID: string): Promise<void> => {
|
||||
await this._axiosClient.delete(`/webhooks/${webhookID}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import * as sdk from '@botpress/sdk'
|
||||
import axios from 'axios'
|
||||
import { commentSchema, formSchema, itemSchema, pageSchema, siteSchema } from 'definitions/sub-schemas'
|
||||
import * as bp from '../.botpress'
|
||||
import { WebflowClient } from './client'
|
||||
import { safeJsonParse } from './utils'
|
||||
|
||||
const webhookRequestSchema = sdk.z.union([
|
||||
sdk.z.object({ triggerType: sdk.z.literal('form_submission'), payload: formSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('site_publish'), payload: siteSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('page_created'), payload: pageSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('page_metadata_updated'), payload: pageSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('page_deleted'), payload: pageSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('collection_item_created'), payload: itemSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('collection_item_deleted'), payload: itemSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('collection_item_changed'), payload: itemSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('collection_item_published'), payload: itemSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('collection_item_unpublished'), payload: itemSchema }),
|
||||
sdk.z.object({ triggerType: sdk.z.literal('comment_created'), payload: commentSchema }),
|
||||
])
|
||||
|
||||
const fireEvent = async <T extends keyof bp.events.Events>(
|
||||
props: bp.HandlerProps,
|
||||
type: T,
|
||||
payload: bp.events.Events[T]
|
||||
) => {
|
||||
await props.client
|
||||
.createEvent({ type: type as Extract<T, string>, payload })
|
||||
.catch(_handleError(`Failed to create ${type} event`))
|
||||
}
|
||||
|
||||
export default new bp.Integration({
|
||||
register: async (props) => {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
const triggerTypesToHook = [
|
||||
'form_submission',
|
||||
'site_publish',
|
||||
'page_created',
|
||||
'page_metadata_updated',
|
||||
'page_deleted',
|
||||
'collection_item_created',
|
||||
'collection_item_changed',
|
||||
'collection_item_deleted',
|
||||
'collection_item_published',
|
||||
'collection_item_unpublished',
|
||||
'comment_created',
|
||||
]
|
||||
|
||||
const already = await client
|
||||
.listWebhooks(props.ctx.configuration.siteID)
|
||||
.catch(_handleError('Failed to list webhooks'))
|
||||
|
||||
const existing = new Set(already.map((w: { triggerType: string }) => w.triggerType))
|
||||
const missing = triggerTypesToHook.filter((t) => !existing.has(t))
|
||||
await Promise.all(
|
||||
missing.map((triggerType) =>
|
||||
client
|
||||
.createWebhook(triggerType, props.ctx.configuration.siteID, props.webhookUrl)
|
||||
.catch(_handleError('Failed to create webhooks'))
|
||||
)
|
||||
)
|
||||
},
|
||||
unregister: async (props) => {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
|
||||
const webhooks = await client
|
||||
.listWebhooks(props.ctx.configuration.siteID)
|
||||
.catch(_handleError('Failed to create webhooks'))
|
||||
|
||||
const webhookIDs = webhooks.map((w: { id: string }) => w.id)
|
||||
await Promise.all(
|
||||
webhookIDs.map((webhookID) => client.deleteWebhooks(webhookID).catch(_handleError('Failed to delete webhook')))
|
||||
)
|
||||
},
|
||||
actions: {
|
||||
async listCollections(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.listCollections(props.ctx.configuration.siteID)
|
||||
.catch(_handleError('Failed to list collections'))
|
||||
},
|
||||
|
||||
async getCollectionDetails(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.getCollectionDetails(props.input.collectionID)
|
||||
.catch(_handleError('Failed to get collection details'))
|
||||
},
|
||||
|
||||
async createCollection(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.createCollection(props.ctx.configuration.siteID, props.input.collectionInfo)
|
||||
.catch(_handleError('Failed to create collection'))
|
||||
},
|
||||
|
||||
async deleteCollection(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client.deleteCollection(props.input.collectionID).catch(_handleError('Failed to delete collection'))
|
||||
},
|
||||
|
||||
async listItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.listItems(props.input.collectionID, props.input.pagination?.offset ?? 0, props.input.pagination?.limit ?? 100)
|
||||
.catch(_handleError('Failed to list items'))
|
||||
},
|
||||
|
||||
async getItem(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.getItem(props.input.collectionID, props.input.itemID, props.input.isLiveItems)
|
||||
.catch(_handleError('Failed to get item'))
|
||||
},
|
||||
|
||||
async createItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.createItems(props.input.collectionID, props.input.items, props.input.isLiveItems)
|
||||
.catch(_handleError('Failed to create items'))
|
||||
},
|
||||
|
||||
async updateItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.updateItem(props.input.collectionID, props.input.items, props.input.isLiveItems)
|
||||
.catch(_handleError('Failed to update items'))
|
||||
},
|
||||
|
||||
async deleteItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.deleteItem(props.input.collectionID, props.input.itemIDs)
|
||||
.catch(_handleError('Failed to delete items'))
|
||||
},
|
||||
|
||||
async publishItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.publishItems(props.input.collectionID, props.input.itemIds)
|
||||
.catch(_handleError('Failed to publish items'))
|
||||
},
|
||||
|
||||
async unpublishLiveItems(props) {
|
||||
const client = new WebflowClient(props.ctx.configuration.apiToken)
|
||||
return await client
|
||||
.unpublishLiveItems(props.input.collectionID, props.input.itemIds)
|
||||
.catch(_handleError('Failed to unpublish live items'))
|
||||
},
|
||||
},
|
||||
channels: {},
|
||||
handler: async (props) => {
|
||||
if (!props.req.body) {
|
||||
props.logger.error('Handler received an empty body')
|
||||
return
|
||||
}
|
||||
|
||||
const jsonParseResult = safeJsonParse(props.req.body)
|
||||
if (!jsonParseResult.success) {
|
||||
props.logger.error(`Failed to parse request body: ${jsonParseResult.err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
const zodParseResult = webhookRequestSchema.safeParse(jsonParseResult.data)
|
||||
if (!zodParseResult.success) {
|
||||
props.logger.error(`Failed to validate request body: ${zodParseResult.error.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = zodParseResult
|
||||
|
||||
switch (data.triggerType) {
|
||||
case 'form_submission':
|
||||
await fireEvent(props, 'formSubmission', data.payload)
|
||||
break
|
||||
case 'site_publish':
|
||||
await fireEvent(props, 'sitePublish', data.payload)
|
||||
break
|
||||
case 'page_created':
|
||||
await fireEvent(props, 'pageCreated', data.payload)
|
||||
break
|
||||
case 'page_metadata_updated':
|
||||
await fireEvent(props, 'pageMetadataUpdated', data.payload)
|
||||
break
|
||||
case 'page_deleted':
|
||||
await fireEvent(props, 'pageDeleted', data.payload)
|
||||
break
|
||||
case 'collection_item_created':
|
||||
await fireEvent(props, 'collectionItemCreated', data.payload)
|
||||
break
|
||||
case 'collection_item_deleted':
|
||||
await fireEvent(props, 'collectionItemDeleted', data.payload)
|
||||
break
|
||||
case 'collection_item_published':
|
||||
await fireEvent(props, 'collectionItemPublished', data.payload)
|
||||
break
|
||||
case 'collection_item_unpublished':
|
||||
await fireEvent(props, 'collectionItemUnpublished', data.payload)
|
||||
break
|
||||
case 'collection_item_changed':
|
||||
await fireEvent(props, 'collectionItemUpdated', data.payload)
|
||||
break
|
||||
case 'comment_created':
|
||||
await fireEvent(props, 'commentCreated', data.payload)
|
||||
break
|
||||
default:
|
||||
data satisfies never
|
||||
props.logger.info(`event ${(data as any).triggerType} not supported`)
|
||||
break
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const _handleError = (outterMessage: string) => (thrown: unknown) => {
|
||||
let innerMessage: string | undefined = undefined
|
||||
if (axios.isAxiosError(thrown)) {
|
||||
innerMessage = thrown.response?.data?.message || thrown.message
|
||||
} else {
|
||||
const err = thrown instanceof Error ? thrown : new Error(String(thrown))
|
||||
innerMessage = err.message
|
||||
}
|
||||
|
||||
const fullMessage = innerMessage ? `${outterMessage}: ${innerMessage}` : outterMessage
|
||||
throw new sdk.RuntimeError(fullMessage)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const safeJsonParse = <T extends unknown>(
|
||||
str: string
|
||||
): { success: true; data: T } | { success: false; err: Error } => {
|
||||
try {
|
||||
const object: T = JSON.parse(str)
|
||||
return { success: true, data: object }
|
||||
} catch (thrown) {
|
||||
const err = thrown instanceof Error ? thrown : new Error(String(thrown))
|
||||
return { success: false, err }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": { "*": ["./*"] },
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": [".botpress/**/*", "definitions/**/*", "src/**/*", "*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import config from '../../vitest.config'
|
||||
export default config
|
||||
Reference in New Issue
Block a user