mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-18 03:18:01 -05:00
Compare commits
7 Commits
fix/block-
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7572185a | ||
|
|
19a8daedf7 | ||
|
|
0fcd52683a | ||
|
|
b8b20576d3 | ||
|
|
4b8534ebd0 | ||
|
|
f6960a4bd4 | ||
|
|
8740566f6a |
@@ -1535,4 +1535,800 @@ Delete a GitHub Project V2. This action is permanent and cannot be undone. Requi
|
||||
| `number` | number | Deleted project number |
|
||||
| `url` | string | Deleted project URL |
|
||||
|
||||
### `github_search_code`
|
||||
|
||||
Search for code across GitHub repositories. Use qualifiers like repo:owner/name, language:js, path:src, extension:py
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | Search query with optional qualifiers \(repo:, language:, path:, extension:, user:, org:\) |
|
||||
| `sort` | string | No | Sort by indexed date \(default: best match\) |
|
||||
| `order` | string | No | Sort order: asc or desc \(default: desc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of code matches from GitHub API |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `path` | string | File path |
|
||||
| ↳ `sha` | string | Blob SHA |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `repository` | object | Repository object |
|
||||
|
||||
### `github_search_commits`
|
||||
|
||||
Search for commits across GitHub. Use qualifiers like repo:owner/name, author:user, committer:user, author-date:>2023-01-01
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | Search query with optional qualifiers \(repo:, author:, committer:, author-date:, committer-date:, merge:true/false\) |
|
||||
| `sort` | string | No | Sort by: author-date or committer-date \(default: best match\) |
|
||||
| `order` | string | No | Sort order: asc or desc \(default: desc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of commit objects from GitHub API |
|
||||
| ↳ `sha` | string | Commit SHA |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `commit` | object | Commit data |
|
||||
| ↳ `author` | object | GitHub user |
|
||||
| ↳ `committer` | object | GitHub user |
|
||||
| ↳ `repository` | object | Repository |
|
||||
|
||||
### `github_search_issues`
|
||||
|
||||
Search for issues and pull requests across GitHub. Use qualifiers like repo:owner/name, is:issue, is:pr, state:open, label:bug, author:user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | Search query with optional qualifiers \(repo:, is:issue, is:pr, state:, label:, author:, assignee:\) |
|
||||
| `sort` | string | No | Sort by: comments, reactions, created, updated, interactions \(default: best match\) |
|
||||
| `order` | string | No | Sort order: asc or desc \(default: desc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of issue/PR objects from GitHub API |
|
||||
| ↳ `id` | number | Issue ID |
|
||||
| ↳ `number` | number | Issue number |
|
||||
| ↳ `title` | string | Title |
|
||||
| ↳ `state` | string | State |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `body` | string | Body text |
|
||||
| ↳ `user` | object | Author |
|
||||
| ↳ `labels` | array | Labels |
|
||||
| ↳ `assignees` | array | Assignees |
|
||||
| ↳ `created_at` | string | Creation date |
|
||||
| ↳ `updated_at` | string | Update date |
|
||||
| ↳ `closed_at` | string | Close date |
|
||||
|
||||
### `github_search_repos`
|
||||
|
||||
Search for repositories across GitHub. Use qualifiers like language:python, stars:>1000, topic:react, user:owner, org:name
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | Search query with optional qualifiers \(language:, stars:, forks:, topic:, user:, org:, in:name,description,readme\) |
|
||||
| `sort` | string | No | Sort by: stars, forks, help-wanted-issues, updated \(default: best match\) |
|
||||
| `order` | string | No | Sort order: asc or desc \(default: desc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of repository objects from GitHub API |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `full_name` | string | Full name |
|
||||
| ↳ `description` | string | Description |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `stargazers_count` | number | Stars |
|
||||
| ↳ `forks_count` | number | Forks |
|
||||
| ↳ `open_issues_count` | number | Open issues |
|
||||
| ↳ `language` | string | Language |
|
||||
| ↳ `topics` | array | Topics |
|
||||
| ↳ `owner` | object | Owner |
|
||||
|
||||
### `github_search_users`
|
||||
|
||||
Search for users and organizations on GitHub. Use qualifiers like type:user, type:org, followers:>1000, repos:>10, location:city
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `q` | string | Yes | Search query with optional qualifiers \(type:user/org, followers:, repos:, location:, language:, created:\) |
|
||||
| `sort` | string | No | Sort by: followers, repositories, joined \(default: best match\) |
|
||||
| `order` | string | No | Sort order: asc or desc \(default: desc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of user objects from GitHub API |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `avatar_url` | string | Avatar URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | Is site admin |
|
||||
|
||||
### `github_list_commits`
|
||||
|
||||
List commits in a repository with optional filtering by SHA, path, author, committer, or date range
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `sha` | string | No | SHA or branch to start listing commits from |
|
||||
| `path` | string | No | Only commits containing this file path |
|
||||
| `author` | string | No | GitHub login or email address to filter by author |
|
||||
| `committer` | string | No | GitHub login or email address to filter by committer |
|
||||
| `since` | string | No | Only commits after this date \(ISO 8601 format\) |
|
||||
| `until` | string | No | Only commits before this date \(ISO 8601 format\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of commit objects from GitHub API |
|
||||
| ↳ `sha` | string | Commit SHA |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `commit` | object | Commit data |
|
||||
| ↳ `author` | object | GitHub user |
|
||||
| ↳ `committer` | object | GitHub user |
|
||||
| ↳ `parents` | array | Parent commits |
|
||||
| `count` | number | Number of commits returned |
|
||||
|
||||
### `github_get_commit`
|
||||
|
||||
Get detailed information about a specific commit including files changed and stats
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `ref` | string | Yes | Commit SHA, branch name, or tag name |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sha` | string | Commit SHA |
|
||||
| `html_url` | string | Web URL |
|
||||
| `commit` | object | Commit data |
|
||||
| `author` | object | GitHub user |
|
||||
| `committer` | object | GitHub user |
|
||||
| `stats` | object | Change stats |
|
||||
| `files` | array | Changed files |
|
||||
| `parents` | array | Parent commits |
|
||||
|
||||
### `github_compare_commits`
|
||||
|
||||
Compare two commits or branches to see the diff, commits between them, and changed files
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `base` | string | Yes | Base branch/tag/SHA for comparison |
|
||||
| `head` | string | Yes | Head branch/tag/SHA for comparison |
|
||||
| `per_page` | number | No | Results per page for files \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number for files \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `status` | string | Comparison status |
|
||||
| `ahead_by` | number | Commits ahead |
|
||||
| `behind_by` | number | Commits behind |
|
||||
| `total_commits` | number | Total commits |
|
||||
| `html_url` | string | Web URL |
|
||||
| `diff_url` | string | Diff URL |
|
||||
| `patch_url` | string | Patch URL |
|
||||
| `base_commit` | object | Base commit |
|
||||
| `merge_base_commit` | object | Merge base |
|
||||
| `commits` | array | Commits between |
|
||||
| `files` | array | Changed files |
|
||||
|
||||
### `github_create_gist`
|
||||
|
||||
Create a new gist with one or more files
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `description` | string | No | Description of the gist |
|
||||
| `files` | json | Yes | JSON object with filenames as keys and content as values. Example: \{"file.txt": \{"content": "Hello"\}\} |
|
||||
| `Example` | string | No | No description |
|
||||
| `public` | boolean | No | Whether the gist is public \(default: false\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Gist ID |
|
||||
| `html_url` | string | Web URL |
|
||||
| `git_pull_url` | string | Git pull URL |
|
||||
| `git_push_url` | string | Git push URL |
|
||||
| `description` | string | Description |
|
||||
| `public` | boolean | Is public |
|
||||
| `created_at` | string | Creation date |
|
||||
| `updated_at` | string | Update date |
|
||||
| `files` | object | Files in gist |
|
||||
| `owner` | object | Owner info |
|
||||
|
||||
### `github_get_gist`
|
||||
|
||||
Get a gist by ID including its file contents
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Gist ID |
|
||||
| `html_url` | string | Web URL |
|
||||
| `description` | string | Description |
|
||||
| `public` | boolean | Is public |
|
||||
| `created_at` | string | Creation date |
|
||||
| `updated_at` | string | Update date |
|
||||
| `files` | object | Files with content |
|
||||
| `owner` | object | Owner info |
|
||||
| `comments` | number | Comment count |
|
||||
|
||||
### `github_list_gists`
|
||||
|
||||
List gists for a user or the authenticated user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `username` | string | No | GitHub username \(omit for authenticated user's gists\) |
|
||||
| `since` | string | No | Only gists updated after this time \(ISO 8601\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of gist objects from GitHub API |
|
||||
| ↳ `id` | string | Gist ID |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `description` | string | Description |
|
||||
| ↳ `public` | boolean | Is public |
|
||||
| ↳ `files` | object | Files |
|
||||
| ↳ `owner` | object | Owner |
|
||||
| `count` | number | Number of gists returned |
|
||||
|
||||
### `github_update_gist`
|
||||
|
||||
Update a gist description or files. To delete a file, set its value to null in files object
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID to update |
|
||||
| `description` | string | No | New description for the gist |
|
||||
| `files` | json | No | JSON object with filenames as keys. Set to null to delete, or provide content to update/add |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Gist ID |
|
||||
| `html_url` | string | Web URL |
|
||||
| `description` | string | Description |
|
||||
| `public` | boolean | Is public |
|
||||
| `updated_at` | string | Update date |
|
||||
| `files` | object | Current files |
|
||||
|
||||
### `github_delete_gist`
|
||||
|
||||
Delete a gist by ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID to delete |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether deletion succeeded |
|
||||
| `gist_id` | string | The deleted gist ID |
|
||||
|
||||
### `github_fork_gist`
|
||||
|
||||
Fork a gist to create your own copy
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID to fork |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | New gist ID |
|
||||
| `html_url` | string | Web URL |
|
||||
| `description` | string | Description |
|
||||
| `public` | boolean | Is public |
|
||||
| `created_at` | string | Creation date |
|
||||
| `owner` | object | Owner info |
|
||||
| `files` | object | Files |
|
||||
|
||||
### `github_star_gist`
|
||||
|
||||
Star a gist
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID to star |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `starred` | boolean | Whether starring succeeded |
|
||||
| `gist_id` | string | The gist ID |
|
||||
|
||||
### `github_unstar_gist`
|
||||
|
||||
Unstar a gist
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `gist_id` | string | Yes | The gist ID to unstar |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `unstarred` | boolean | Whether unstarring succeeded |
|
||||
| `gist_id` | string | The gist ID |
|
||||
|
||||
### `github_fork_repo`
|
||||
|
||||
Fork a repository to your account or an organization
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner to fork from |
|
||||
| `repo` | string | Yes | Repository name to fork |
|
||||
| `organization` | string | No | Organization to fork into \(omit to fork to your account\) |
|
||||
| `name` | string | No | Custom name for the forked repository |
|
||||
| `default_branch_only` | boolean | No | Only fork the default branch \(default: false\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Repository ID |
|
||||
| `full_name` | string | Full name |
|
||||
| `html_url` | string | Web URL |
|
||||
| `clone_url` | string | Clone URL |
|
||||
| `ssh_url` | string | SSH URL |
|
||||
| `default_branch` | string | Default branch |
|
||||
| `fork` | boolean | Is a fork |
|
||||
| `parent` | object | Parent repository |
|
||||
| `owner` | object | Owner |
|
||||
|
||||
### `github_list_forks`
|
||||
|
||||
List forks of a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `sort` | string | No | Sort by: newest, oldest, stargazers, watchers \(default: newest\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of fork repository objects from GitHub API |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `full_name` | string | Full name |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `owner` | object | Owner |
|
||||
| ↳ `stargazers_count` | number | Stars |
|
||||
| ↳ `forks_count` | number | Forks |
|
||||
| `count` | number | Number of forks returned |
|
||||
|
||||
### `github_create_milestone`
|
||||
|
||||
Create a milestone in a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `title` | string | Yes | Milestone title |
|
||||
| `state` | string | No | State: open or closed \(default: open\) |
|
||||
| `description` | string | No | Milestone description |
|
||||
| `due_on` | string | No | Due date \(ISO 8601 format, e.g., 2024-12-31T23:59:59Z\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `number` | number | Milestone number |
|
||||
| `title` | string | Title |
|
||||
| `description` | string | Description |
|
||||
| `state` | string | State |
|
||||
| `html_url` | string | Web URL |
|
||||
| `due_on` | string | Due date |
|
||||
| `open_issues` | number | Open issues |
|
||||
| `closed_issues` | number | Closed issues |
|
||||
| `creator` | object | Creator |
|
||||
|
||||
### `github_get_milestone`
|
||||
|
||||
Get a specific milestone by number
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `milestone_number` | number | Yes | Milestone number |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `number` | number | Milestone number |
|
||||
| `title` | string | Title |
|
||||
| `description` | string | Description |
|
||||
| `state` | string | State |
|
||||
| `html_url` | string | Web URL |
|
||||
| `due_on` | string | Due date |
|
||||
| `open_issues` | number | Open issues |
|
||||
| `closed_issues` | number | Closed issues |
|
||||
| `closed_at` | string | Close date |
|
||||
| `creator` | object | Creator |
|
||||
|
||||
### `github_list_milestones`
|
||||
|
||||
List milestones in a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `state` | string | No | Filter by state: open, closed, all \(default: open\) |
|
||||
| `sort` | string | No | Sort by: due_on or completeness \(default: due_on\) |
|
||||
| `direction` | string | No | Sort direction: asc or desc \(default: asc\) |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of milestone objects from GitHub API |
|
||||
| ↳ `number` | number | Milestone number |
|
||||
| ↳ `title` | string | Title |
|
||||
| ↳ `state` | string | State |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `open_issues` | number | Open issues |
|
||||
| ↳ `closed_issues` | number | Closed issues |
|
||||
| `count` | number | Number of milestones returned |
|
||||
|
||||
### `github_update_milestone`
|
||||
|
||||
Update a milestone in a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `milestone_number` | number | Yes | Milestone number to update |
|
||||
| `title` | string | No | New milestone title |
|
||||
| `state` | string | No | New state: open or closed |
|
||||
| `description` | string | No | New description |
|
||||
| `due_on` | string | No | New due date \(ISO 8601 format\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `number` | number | Milestone number |
|
||||
| `title` | string | Title |
|
||||
| `description` | string | Description |
|
||||
| `state` | string | State |
|
||||
| `html_url` | string | Web URL |
|
||||
| `due_on` | string | Due date |
|
||||
| `open_issues` | number | Open issues |
|
||||
| `closed_issues` | number | Closed issues |
|
||||
|
||||
### `github_delete_milestone`
|
||||
|
||||
Delete a milestone from a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `milestone_number` | number | Yes | Milestone number to delete |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether deletion succeeded |
|
||||
| `milestone_number` | number | The deleted milestone number |
|
||||
|
||||
### `github_create_issue_reaction`
|
||||
|
||||
Add a reaction to an issue
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `issue_number` | number | Yes | Issue number |
|
||||
| `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Reaction ID |
|
||||
| `user` | object | User who reacted |
|
||||
| `content` | string | Reaction type |
|
||||
| `created_at` | string | Creation date |
|
||||
|
||||
### `github_delete_issue_reaction`
|
||||
|
||||
Remove a reaction from an issue
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `issue_number` | number | Yes | Issue number |
|
||||
| `reaction_id` | number | Yes | Reaction ID to delete |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether deletion succeeded |
|
||||
| `reaction_id` | number | The deleted reaction ID |
|
||||
|
||||
### `github_create_comment_reaction`
|
||||
|
||||
Add a reaction to an issue comment
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `comment_id` | number | Yes | Comment ID |
|
||||
| `content` | string | Yes | Reaction type: +1 \(thumbs up\), -1 \(thumbs down\), laugh, confused, heart, hooray, rocket, eyes |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | number | Reaction ID |
|
||||
| `user` | object | User who reacted |
|
||||
| `content` | string | Reaction type |
|
||||
| `created_at` | string | Creation date |
|
||||
|
||||
### `github_delete_comment_reaction`
|
||||
|
||||
Remove a reaction from an issue comment
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `comment_id` | number | Yes | Comment ID |
|
||||
| `reaction_id` | number | Yes | Reaction ID to delete |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether deletion succeeded |
|
||||
| `reaction_id` | number | The deleted reaction ID |
|
||||
|
||||
### `github_star_repo`
|
||||
|
||||
Star a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `starred` | boolean | Whether starring succeeded |
|
||||
| `owner` | string | Repository owner |
|
||||
| `repo` | string | Repository name |
|
||||
|
||||
### `github_unstar_repo`
|
||||
|
||||
Remove star from a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `unstarred` | boolean | Whether unstarring succeeded |
|
||||
| `owner` | string | Repository owner |
|
||||
| `repo` | string | Repository name |
|
||||
|
||||
### `github_check_star`
|
||||
|
||||
Check if you have starred a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `starred` | boolean | Whether you have starred the repo |
|
||||
| `owner` | string | Repository owner |
|
||||
| `repo` | string | Repository name |
|
||||
|
||||
### `github_list_stargazers`
|
||||
|
||||
List users who have starred a repository
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `owner` | string | Yes | Repository owner |
|
||||
| `repo` | string | Yes | Repository name |
|
||||
| `per_page` | number | No | Results per page \(max 100, default: 30\) |
|
||||
| `page` | number | No | Page number \(default: 1\) |
|
||||
| `apiKey` | string | Yes | GitHub API token |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of user objects from GitHub API |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `avatar_url` | string | Avatar URL |
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| `count` | number | Number of stargazers returned |
|
||||
|
||||
|
||||
|
||||
@@ -119,6 +119,145 @@ Get a specific event from Google Calendar. Returns API-aligned fields only.
|
||||
| `creator` | json | Event creator |
|
||||
| `organizer` | json | Event organizer |
|
||||
|
||||
### `google_calendar_update`
|
||||
|
||||
Update an existing event in Google Calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Event ID to update |
|
||||
| `summary` | string | No | New event title/summary |
|
||||
| `description` | string | No | New event description |
|
||||
| `location` | string | No | New event location |
|
||||
| `startDateTime` | string | No | New start date and time. MUST include timezone offset \(e.g., 2025-06-03T10:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `endDateTime` | string | No | New end date and time. MUST include timezone offset \(e.g., 2025-06-03T11:00:00-08:00\) OR provide timeZone parameter |
|
||||
| `timeZone` | string | No | Time zone \(e.g., America/Los_Angeles\). Required if datetime does not include offset. |
|
||||
| `attendees` | array | No | Array of attendee email addresses \(replaces existing attendees\) |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Event ID |
|
||||
| `htmlLink` | string | Event link |
|
||||
| `status` | string | Event status |
|
||||
| `summary` | string | Event title |
|
||||
| `description` | string | Event description |
|
||||
| `location` | string | Event location |
|
||||
| `start` | json | Event start |
|
||||
| `end` | json | Event end |
|
||||
| `attendees` | json | Event attendees |
|
||||
| `creator` | json | Event creator |
|
||||
| `organizer` | json | Event organizer |
|
||||
|
||||
### `google_calendar_delete`
|
||||
|
||||
Delete an event from Google Calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Event ID to delete |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `eventId` | string | Deleted event ID |
|
||||
| `deleted` | boolean | Whether deletion was successful |
|
||||
|
||||
### `google_calendar_move`
|
||||
|
||||
Move an event to a different calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Source calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Event ID to move |
|
||||
| `destinationCalendarId` | string | Yes | Destination calendar ID |
|
||||
| `sendUpdates` | string | No | How to send updates to attendees: all, externalOnly, or none |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Event ID |
|
||||
| `htmlLink` | string | Event link |
|
||||
| `status` | string | Event status |
|
||||
| `summary` | string | Event title |
|
||||
| `description` | string | Event description |
|
||||
| `location` | string | Event location |
|
||||
| `start` | json | Event start |
|
||||
| `end` | json | Event end |
|
||||
| `attendees` | json | Event attendees |
|
||||
| `creator` | json | Event creator |
|
||||
| `organizer` | json | Event organizer |
|
||||
|
||||
### `google_calendar_instances`
|
||||
|
||||
Get instances of a recurring event from Google Calendar. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `calendarId` | string | No | Calendar ID \(defaults to primary\) |
|
||||
| `eventId` | string | Yes | Recurring event ID to get instances of |
|
||||
| `timeMin` | string | No | Lower bound for instances \(RFC3339 timestamp, e.g., 2025-06-03T00:00:00Z\) |
|
||||
| `timeMax` | string | No | Upper bound for instances \(RFC3339 timestamp, e.g., 2025-06-04T00:00:00Z\) |
|
||||
| `maxResults` | number | No | Maximum number of instances to return \(default 250, max 2500\) |
|
||||
| `pageToken` | string | No | Token for retrieving subsequent pages of results |
|
||||
| `showDeleted` | boolean | No | Include deleted instances |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `nextPageToken` | string | Next page token |
|
||||
| `timeZone` | string | Calendar time zone |
|
||||
| `instances` | json | List of recurring event instances |
|
||||
|
||||
### `google_calendar_list_calendars`
|
||||
|
||||
List all calendars in the user
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `minAccessRole` | string | No | Minimum access role for returned calendars: freeBusyReader, reader, writer, or owner |
|
||||
| `maxResults` | number | No | Maximum number of calendars to return \(default 100, max 250\) |
|
||||
| `pageToken` | string | No | Token for retrieving subsequent pages of results |
|
||||
| `showDeleted` | boolean | No | Include deleted calendars |
|
||||
| `showHidden` | boolean | No | Include hidden calendars |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `nextPageToken` | string | Next page token |
|
||||
| `calendars` | array | List of calendars |
|
||||
| ↳ `id` | string | Calendar ID |
|
||||
| ↳ `summary` | string | Calendar title |
|
||||
| ↳ `description` | string | Calendar description |
|
||||
| ↳ `location` | string | Calendar location |
|
||||
| ↳ `timeZone` | string | Calendar time zone |
|
||||
| ↳ `accessRole` | string | Access role for the calendar |
|
||||
| ↳ `backgroundColor` | string | Calendar background color |
|
||||
| ↳ `foregroundColor` | string | Calendar foreground color |
|
||||
| ↳ `primary` | boolean | Whether this is the primary calendar |
|
||||
| ↳ `hidden` | boolean | Whether the calendar is hidden |
|
||||
| ↳ `selected` | boolean | Whether the calendar is selected |
|
||||
|
||||
### `google_calendar_quick_add`
|
||||
|
||||
Create events from natural language text. Returns API-aligned fields only.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Google Drive
|
||||
description: Create, upload, and list files
|
||||
description: Manage files, folders, and permissions
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -40,12 +40,178 @@ In Sim, the Google Drive integration enables your agents to interact directly wi
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Drive into the workflow. Can create, upload, and list files.
|
||||
Integrate Google Drive into the workflow. Can create, upload, download, copy, move, delete, share files and manage permissions.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `google_drive_list`
|
||||
|
||||
List files and folders in Google Drive with complete metadata
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderSelector` | string | No | Select the folder to list files from |
|
||||
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
|
||||
| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. |
|
||||
| `pageSize` | number | No | The maximum number of files to return \(default: 100\) |
|
||||
| `pageToken` | string | No | The page token to use for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `files` | array | Array of file metadata objects from Google Drive |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `originalFilename` | string | Original uploaded filename |
|
||||
| ↳ `fullFileExtension` | string | Full file extension |
|
||||
| ↳ `fileExtension` | string | File extension |
|
||||
| ↳ `owners` | json | List of file owners |
|
||||
| ↳ `permissions` | json | File permissions |
|
||||
| ↳ `permissionIds` | json | Permission IDs |
|
||||
| ↳ `shared` | boolean | Whether file is shared |
|
||||
| ↳ `ownedByMe` | boolean | Whether owned by current user |
|
||||
| ↳ `writersCanShare` | boolean | Whether writers can share |
|
||||
| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy |
|
||||
| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission |
|
||||
| ↳ `sharingUser` | json | User who shared the file |
|
||||
| ↳ `starred` | boolean | Whether file is starred |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash |
|
||||
| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed |
|
||||
| ↳ `appProperties` | json | App-specific properties |
|
||||
| ↳ `createdTime` | string | File creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `modifiedByMeTime` | string | When modified by current user |
|
||||
| ↳ `viewedByMeTime` | string | When last viewed by current user |
|
||||
| ↳ `sharedWithMeTime` | string | When shared with current user |
|
||||
| ↳ `lastModifyingUser` | json | User who last modified the file |
|
||||
| ↳ `viewedByMe` | boolean | Whether viewed by current user |
|
||||
| ↳ `modifiedByMe` | boolean | Whether modified by current user |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `webContentLink` | string | Direct download URL |
|
||||
| ↳ `iconLink` | string | URL to file icon |
|
||||
| ↳ `thumbnailLink` | string | URL to thumbnail |
|
||||
| ↳ `exportLinks` | json | Export format links |
|
||||
| ↳ `size` | string | File size in bytes |
|
||||
| ↳ `quotaBytesUsed` | string | Storage quota used |
|
||||
| ↳ `md5Checksum` | string | MD5 hash |
|
||||
| ↳ `sha1Checksum` | string | SHA-1 hash |
|
||||
| ↳ `sha256Checksum` | string | SHA-256 hash |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `spaces` | json | Spaces containing file |
|
||||
| ↳ `driveId` | string | Shared drive ID |
|
||||
| ↳ `capabilities` | json | User capabilities on file |
|
||||
| ↳ `version` | string | Version number |
|
||||
| ↳ `headRevisionId` | string | Head revision ID |
|
||||
| ↳ `hasThumbnail` | boolean | Whether has thumbnail |
|
||||
| ↳ `thumbnailVersion` | string | Thumbnail version |
|
||||
| ↳ `imageMediaMetadata` | json | Image-specific metadata |
|
||||
| ↳ `videoMediaMetadata` | json | Video-specific metadata |
|
||||
| ↳ `isAppAuthorized` | boolean | Whether created by requesting app |
|
||||
| ↳ `contentRestrictions` | json | Content restrictions |
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
| `nextPageToken` | string | Token for fetching the next page of results |
|
||||
|
||||
### `google_drive_get_file`
|
||||
|
||||
Get metadata for a specific file in Google Drive by its ID
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `size` | string | File size in bytes |
|
||||
| ↳ `starred` | boolean | Whether file is starred |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `webContentLink` | string | Direct download URL |
|
||||
| ↳ `iconLink` | string | URL to file icon |
|
||||
| ↳ `thumbnailLink` | string | URL to thumbnail |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `owners` | json | List of file owners |
|
||||
| ↳ `permissions` | json | File permissions |
|
||||
| ↳ `createdTime` | string | File creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `lastModifyingUser` | json | User who last modified the file |
|
||||
| ↳ `shared` | boolean | Whether file is shared |
|
||||
| ↳ `ownedByMe` | boolean | Whether owned by current user |
|
||||
| ↳ `capabilities` | json | User capabilities on file |
|
||||
| ↳ `md5Checksum` | string | MD5 hash |
|
||||
| ↳ `version` | string | Version number |
|
||||
|
||||
### `google_drive_create_folder`
|
||||
|
||||
Create a new folder in Google Drive with complete metadata returned
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | Name of the folder to create |
|
||||
| `folderSelector` | string | No | Select the parent folder to create the folder in |
|
||||
| `folderId` | string | No | ID of the parent folder \(internal use\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | object | Complete created folder metadata from Google Drive |
|
||||
| ↳ `id` | string | Google Drive folder ID |
|
||||
| ↳ `name` | string | Folder name |
|
||||
| ↳ `mimeType` | string | MIME type \(application/vnd.google-apps.folder\) |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | Folder description |
|
||||
| ↳ `owners` | json | List of folder owners |
|
||||
| ↳ `permissions` | json | Folder permissions |
|
||||
| ↳ `permissionIds` | json | Permission IDs |
|
||||
| ↳ `shared` | boolean | Whether folder is shared |
|
||||
| ↳ `ownedByMe` | boolean | Whether owned by current user |
|
||||
| ↳ `writersCanShare` | boolean | Whether writers can share |
|
||||
| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy |
|
||||
| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission |
|
||||
| ↳ `sharingUser` | json | User who shared the folder |
|
||||
| ↳ `starred` | boolean | Whether folder is starred |
|
||||
| ↳ `trashed` | boolean | Whether folder is in trash |
|
||||
| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed |
|
||||
| ↳ `appProperties` | json | App-specific properties |
|
||||
| ↳ `folderColorRgb` | string | Folder color |
|
||||
| ↳ `createdTime` | string | Folder creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `modifiedByMeTime` | string | When modified by current user |
|
||||
| ↳ `viewedByMeTime` | string | When last viewed by current user |
|
||||
| ↳ `sharedWithMeTime` | string | When shared with current user |
|
||||
| ↳ `lastModifyingUser` | json | User who last modified the folder |
|
||||
| ↳ `viewedByMe` | boolean | Whether viewed by current user |
|
||||
| ↳ `modifiedByMe` | boolean | Whether modified by current user |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `iconLink` | string | URL to folder icon |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `spaces` | json | Spaces containing folder |
|
||||
| ↳ `driveId` | string | Shared drive ID |
|
||||
| ↳ `capabilities` | json | User capabilities on folder |
|
||||
| ↳ `version` | string | Version number |
|
||||
| ↳ `isAppAuthorized` | boolean | Whether created by requesting app |
|
||||
| ↳ `contentRestrictions` | json | Content restrictions |
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
|
||||
### `google_drive_upload`
|
||||
|
||||
Upload a file to Google Drive with complete metadata returned
|
||||
@@ -119,61 +285,6 @@ Upload a file to Google Drive with complete metadata returned
|
||||
| ↳ `contentRestrictions` | json | Content restrictions |
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
|
||||
### `google_drive_create_folder`
|
||||
|
||||
Create a new folder in Google Drive with complete metadata returned
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileName` | string | Yes | Name of the folder to create |
|
||||
| `folderSelector` | string | No | Select the parent folder to create the folder in |
|
||||
| `folderId` | string | No | ID of the parent folder \(internal use\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | object | Complete created folder metadata from Google Drive |
|
||||
| ↳ `id` | string | Google Drive folder ID |
|
||||
| ↳ `name` | string | Folder name |
|
||||
| ↳ `mimeType` | string | MIME type \(application/vnd.google-apps.folder\) |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | Folder description |
|
||||
| ↳ `owners` | json | List of folder owners |
|
||||
| ↳ `permissions` | json | Folder permissions |
|
||||
| ↳ `permissionIds` | json | Permission IDs |
|
||||
| ↳ `shared` | boolean | Whether folder is shared |
|
||||
| ↳ `ownedByMe` | boolean | Whether owned by current user |
|
||||
| ↳ `writersCanShare` | boolean | Whether writers can share |
|
||||
| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy |
|
||||
| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission |
|
||||
| ↳ `sharingUser` | json | User who shared the folder |
|
||||
| ↳ `starred` | boolean | Whether folder is starred |
|
||||
| ↳ `trashed` | boolean | Whether folder is in trash |
|
||||
| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed |
|
||||
| ↳ `appProperties` | json | App-specific properties |
|
||||
| ↳ `folderColorRgb` | string | Folder color |
|
||||
| ↳ `createdTime` | string | Folder creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `modifiedByMeTime` | string | When modified by current user |
|
||||
| ↳ `viewedByMeTime` | string | When last viewed by current user |
|
||||
| ↳ `sharedWithMeTime` | string | When shared with current user |
|
||||
| ↳ `lastModifyingUser` | json | User who last modified the folder |
|
||||
| ↳ `viewedByMe` | boolean | Whether viewed by current user |
|
||||
| ↳ `modifiedByMe` | boolean | Whether modified by current user |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `iconLink` | string | URL to folder icon |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `spaces` | json | Spaces containing folder |
|
||||
| ↳ `driveId` | string | Shared drive ID |
|
||||
| ↳ `capabilities` | json | User capabilities on folder |
|
||||
| ↳ `version` | string | Version number |
|
||||
| ↳ `isAppAuthorized` | boolean | Whether created by requesting app |
|
||||
| ↳ `contentRestrictions` | json | Content restrictions |
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
|
||||
### `google_drive_download`
|
||||
|
||||
Download a file from Google Drive with complete metadata (exports Google Workspace files automatically)
|
||||
@@ -251,77 +362,229 @@ Download a file from Google Drive with complete metadata (exports Google Workspa
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
| ↳ `revisions` | json | File revision history \(first 100 revisions only\) |
|
||||
|
||||
### `google_drive_list`
|
||||
### `google_drive_copy`
|
||||
|
||||
List files and folders in Google Drive with complete metadata
|
||||
Create a copy of a file in Google Drive
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `folderSelector` | string | No | Select the folder to list files from |
|
||||
| `folderId` | string | No | The ID of the folder to list files from \(internal use\) |
|
||||
| `query` | string | No | Search term to filter files by name \(e.g. "budget" finds files with "budget" in the name\). Do NOT use Google Drive query syntax here - just provide a plain search term. |
|
||||
| `pageSize` | number | No | The maximum number of files to return \(default: 100\) |
|
||||
| `pageToken` | string | No | The page token to use for pagination |
|
||||
| `fileId` | string | Yes | The ID of the file to copy |
|
||||
| `newName` | string | No | Name for the copied file \(defaults to "Copy of \[original name\]"\) |
|
||||
| `destinationFolderId` | string | No | ID of the folder to place the copy in \(defaults to same location as original\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `files` | array | Array of file metadata objects from Google Drive |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `originalFilename` | string | Original uploaded filename |
|
||||
| ↳ `fullFileExtension` | string | Full file extension |
|
||||
| ↳ `fileExtension` | string | File extension |
|
||||
| ↳ `owners` | json | List of file owners |
|
||||
| ↳ `permissions` | json | File permissions |
|
||||
| ↳ `permissionIds` | json | Permission IDs |
|
||||
| ↳ `shared` | boolean | Whether file is shared |
|
||||
| ↳ `ownedByMe` | boolean | Whether owned by current user |
|
||||
| ↳ `writersCanShare` | boolean | Whether writers can share |
|
||||
| ↳ `viewersCanCopyContent` | boolean | Whether viewers can copy |
|
||||
| ↳ `copyRequiresWriterPermission` | boolean | Whether copy requires writer permission |
|
||||
| ↳ `sharingUser` | json | User who shared the file |
|
||||
| ↳ `starred` | boolean | Whether file is starred |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash |
|
||||
| ↳ `explicitlyTrashed` | boolean | Whether explicitly trashed |
|
||||
| ↳ `appProperties` | json | App-specific properties |
|
||||
| ↳ `createdTime` | string | File creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `modifiedByMeTime` | string | When modified by current user |
|
||||
| ↳ `viewedByMeTime` | string | When last viewed by current user |
|
||||
| ↳ `sharedWithMeTime` | string | When shared with current user |
|
||||
| ↳ `lastModifyingUser` | json | User who last modified the file |
|
||||
| ↳ `viewedByMe` | boolean | Whether viewed by current user |
|
||||
| ↳ `modifiedByMe` | boolean | Whether modified by current user |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `webContentLink` | string | Direct download URL |
|
||||
| ↳ `iconLink` | string | URL to file icon |
|
||||
| ↳ `thumbnailLink` | string | URL to thumbnail |
|
||||
| ↳ `exportLinks` | json | Export format links |
|
||||
| ↳ `size` | string | File size in bytes |
|
||||
| ↳ `quotaBytesUsed` | string | Storage quota used |
|
||||
| ↳ `md5Checksum` | string | MD5 hash |
|
||||
| ↳ `sha1Checksum` | string | SHA-1 hash |
|
||||
| ↳ `sha256Checksum` | string | SHA-256 hash |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `spaces` | json | Spaces containing file |
|
||||
| ↳ `driveId` | string | Shared drive ID |
|
||||
| ↳ `capabilities` | json | User capabilities on file |
|
||||
| ↳ `version` | string | Version number |
|
||||
| ↳ `headRevisionId` | string | Head revision ID |
|
||||
| ↳ `hasThumbnail` | boolean | Whether has thumbnail |
|
||||
| ↳ `thumbnailVersion` | string | Thumbnail version |
|
||||
| ↳ `imageMediaMetadata` | json | Image-specific metadata |
|
||||
| ↳ `videoMediaMetadata` | json | Video-specific metadata |
|
||||
| ↳ `isAppAuthorized` | boolean | Whether created by requesting app |
|
||||
| ↳ `contentRestrictions` | json | Content restrictions |
|
||||
| ↳ `linkShareMetadata` | json | Link share metadata |
|
||||
| `nextPageToken` | string | Token for fetching the next page of results |
|
||||
| `file` | json | The copied file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID of the copy |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `createdTime` | string | File creation time |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
| ↳ `owners` | json | List of file owners |
|
||||
| ↳ `size` | string | File size in bytes |
|
||||
|
||||
### `google_drive_update`
|
||||
|
||||
Update file metadata in Google Drive (rename, move, star, add description)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to update |
|
||||
| `name` | string | No | New name for the file |
|
||||
| `description` | string | No | New description for the file |
|
||||
| `addParents` | string | No | Comma-separated list of parent folder IDs to add \(moves file to these folders\) |
|
||||
| `removeParents` | string | No | Comma-separated list of parent folder IDs to remove |
|
||||
| `starred` | boolean | No | Whether to star or unstar the file |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The updated file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `starred` | boolean | Whether file is starred |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
| ↳ `modifiedTime` | string | Last modification time |
|
||||
|
||||
### `google_drive_trash`
|
||||
|
||||
Move a file to the trash in Google Drive (can be restored later)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to move to trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The trashed file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash \(should be true\) |
|
||||
| ↳ `trashedTime` | string | When file was trashed |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
|
||||
### `google_drive_untrash`
|
||||
|
||||
Restore a file from the trash in Google Drive
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to restore from trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The restored file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash \(should be false\) |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
|
||||
### `google_drive_delete`
|
||||
|
||||
Permanently delete a file from Google Drive (bypasses trash)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to permanently delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the file was successfully deleted |
|
||||
| `fileId` | string | The ID of the deleted file |
|
||||
|
||||
### `google_drive_share`
|
||||
|
||||
Share a file with a user, group, domain, or make it public
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to share |
|
||||
| `type` | string | Yes | Type of grantee: user, group, domain, or anyone |
|
||||
| `role` | string | Yes | Permission role: owner \(transfer ownership\), organizer \(shared drive only\), fileOrganizer \(shared drive only\), writer \(edit\), commenter \(view and comment\), reader \(view only\) |
|
||||
| `email` | string | No | Email address of the user or group \(required for type=user or type=group\) |
|
||||
| `domain` | string | No | Domain to share with \(required for type=domain\) |
|
||||
| `transferOwnership` | boolean | No | Required when role is owner. Transfers ownership to the specified user. |
|
||||
| `moveToNewOwnersRoot` | boolean | No | When transferring ownership, move the file to the new owner's My Drive root folder. |
|
||||
| `sendNotification` | boolean | No | Whether to send an email notification \(default: true\) |
|
||||
| `emailMessage` | string | No | Custom message to include in the notification email |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `permission` | json | The created permission details |
|
||||
| ↳ `id` | string | Permission ID |
|
||||
| ↳ `type` | string | Grantee type \(user, group, domain, anyone\) |
|
||||
| ↳ `role` | string | Permission role |
|
||||
| ↳ `emailAddress` | string | Email of the grantee |
|
||||
| ↳ `displayName` | string | Display name of the grantee |
|
||||
| ↳ `domain` | string | Domain of the grantee |
|
||||
| ↳ `expirationTime` | string | Expiration time |
|
||||
| ↳ `deleted` | boolean | Whether grantee is deleted |
|
||||
|
||||
### `google_drive_unshare`
|
||||
|
||||
Remove a permission from a file (revoke access)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to modify permissions on |
|
||||
| `permissionId` | string | Yes | The ID of the permission to remove \(use list_permissions to find this\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `removed` | boolean | Whether the permission was successfully removed |
|
||||
| `fileId` | string | The ID of the file |
|
||||
| `permissionId` | string | The ID of the removed permission |
|
||||
|
||||
### `google_drive_list_permissions`
|
||||
|
||||
List all permissions (who has access) for a file in Google Drive
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to list permissions for |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `permissions` | array | List of permissions on the file |
|
||||
| ↳ `id` | string | Permission ID \(use to remove permission\) |
|
||||
| ↳ `type` | string | Grantee type \(user, group, domain, anyone\) |
|
||||
| ↳ `role` | string | Permission role \(owner, organizer, fileOrganizer, writer, commenter, reader\) |
|
||||
| ↳ `emailAddress` | string | Email of the grantee |
|
||||
| ↳ `displayName` | string | Display name of the grantee |
|
||||
| ↳ `photoLink` | string | Photo URL of the grantee |
|
||||
| ↳ `domain` | string | Domain of the grantee |
|
||||
| ↳ `expirationTime` | string | When permission expires |
|
||||
| ↳ `deleted` | boolean | Whether grantee account is deleted |
|
||||
| ↳ `allowFileDiscovery` | boolean | Whether file is discoverable by grantee |
|
||||
| ↳ `pendingOwner` | boolean | Whether ownership transfer is pending |
|
||||
| ↳ `permissionDetails` | json | Details about inherited permissions |
|
||||
|
||||
### `google_drive_get_about`
|
||||
|
||||
Get information about the user and their Google Drive (storage quota, capabilities)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `user` | json | Information about the authenticated user |
|
||||
| ↳ `displayName` | string | User display name |
|
||||
| ↳ `emailAddress` | string | User email address |
|
||||
| ↳ `photoLink` | string | URL to user profile photo |
|
||||
| ↳ `permissionId` | string | User permission ID |
|
||||
| ↳ `me` | boolean | Whether this is the authenticated user |
|
||||
| `storageQuota` | json | Storage quota information in bytes |
|
||||
| ↳ `limit` | string | Total storage limit in bytes \(null for unlimited\) |
|
||||
| ↳ `usage` | string | Total storage used in bytes |
|
||||
| ↳ `usageInDrive` | string | Storage used by Drive files in bytes |
|
||||
| ↳ `usageInDriveTrash` | string | Storage used by trashed files in bytes |
|
||||
| `canCreateDrives` | boolean | Whether user can create shared drives |
|
||||
| `importFormats` | json | Map of MIME types that can be imported and their target formats |
|
||||
| `exportFormats` | json | Map of Google Workspace MIME types and their exportable formats |
|
||||
| `maxUploadSize` | string | Maximum upload size in bytes |
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Google Forms
|
||||
description: Read responses from a Google Form
|
||||
description: Manage Google Forms and responses
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
@@ -29,7 +29,7 @@ In Sim, the Google Forms integration enables your agents to programmatically acc
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Forms into your workflow. Provide a Form ID to list responses, or specify a Response ID to fetch a single response. Requires OAuth.
|
||||
Integrate Google Forms into your workflow. Read form structure, get responses, create forms, update content, and manage notification watches.
|
||||
|
||||
|
||||
|
||||
@@ -37,15 +37,202 @@ Integrate Google Forms into your workflow. Provide a Form ID to list responses,
|
||||
|
||||
### `google_forms_get_responses`
|
||||
|
||||
Retrieve a single response or list responses from a Google Form
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form |
|
||||
| `responseId` | string | No | If provided, returns this specific response |
|
||||
| `pageSize` | number | No | Maximum number of responses to return \(service may return fewer\). Defaults to 5000. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `data` | json | Response or list of responses |
|
||||
| `response` | json | Operation response data |
|
||||
| `formId` | string | Form ID |
|
||||
| `title` | string | Form title |
|
||||
| `responderUri` | string | Form responder URL |
|
||||
| `items` | json | Form items |
|
||||
| `responses` | json | Form responses |
|
||||
| `watches` | json | Form watches |
|
||||
|
||||
### `google_forms_get_form`
|
||||
|
||||
Retrieve a form structure including its items, settings, and metadata
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formId` | string | The form ID |
|
||||
| `title` | string | The form title visible to responders |
|
||||
| `description` | string | The form description |
|
||||
| `documentTitle` | string | The document title visible in Drive |
|
||||
| `responderUri` | string | The URI to share with responders |
|
||||
| `linkedSheetId` | string | The ID of the linked Google Sheet |
|
||||
| `revisionId` | string | The revision ID of the form |
|
||||
| `items` | array | The form items \(questions, sections, etc.\) |
|
||||
| ↳ `itemId` | string | Item ID |
|
||||
| ↳ `title` | string | Item title |
|
||||
| ↳ `description` | string | Item description |
|
||||
| `settings` | json | Form settings |
|
||||
| `publishSettings` | json | Form publish settings |
|
||||
|
||||
### `google_forms_create_form`
|
||||
|
||||
Create a new Google Form with a title
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `title` | string | Yes | The title of the form visible to responders |
|
||||
| `documentTitle` | string | No | The document title visible in Drive \(defaults to form title\) |
|
||||
| `unpublished` | boolean | No | If true, create an unpublished form that does not accept responses |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formId` | string | The ID of the created form |
|
||||
| `title` | string | The form title |
|
||||
| `documentTitle` | string | The document title in Drive |
|
||||
| `responderUri` | string | The URI to share with responders |
|
||||
| `revisionId` | string | The revision ID of the form |
|
||||
|
||||
### `google_forms_batch_update`
|
||||
|
||||
Apply multiple updates to a form (add items, update info, change settings, etc.)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form to update |
|
||||
| `requests` | json | Yes | Array of update requests \(updateFormInfo, updateSettings, createItem, updateItem, moveItem, deleteItem\) |
|
||||
| `includeFormInResponse` | boolean | No | Whether to return the updated form in the response |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `replies` | array | The replies from each update request |
|
||||
| `writeControl` | json | Write control information with revision IDs |
|
||||
| `form` | json | The updated form \(if includeFormInResponse was true\) |
|
||||
|
||||
### `google_forms_set_publish_settings`
|
||||
|
||||
Update the publish settings of a form (publish/unpublish, accept responses)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form |
|
||||
| `isPublished` | boolean | Yes | Whether the form is published and visible to others |
|
||||
| `isAcceptingResponses` | boolean | No | Whether the form accepts responses \(forced to false if isPublished is false\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `formId` | string | The form ID |
|
||||
| `publishSettings` | json | The updated publish settings |
|
||||
| ↳ `publishState` | object | The publish state |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
|
||||
|
||||
### `google_forms_create_watch`
|
||||
|
||||
Create a notification watch for form changes (schema changes or new responses)
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form to watch |
|
||||
| `eventType` | string | Yes | Event type to watch: SCHEMA \(form changes\) or RESPONSES \(new submissions\) |
|
||||
| `topicName` | string | Yes | The Cloud Pub/Sub topic name \(format: projects/\{project\}/topics/\{topic\}\) |
|
||||
| `watchId` | string | No | Custom watch ID \(4-63 chars, lowercase letters, numbers, hyphens\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The watch ID |
|
||||
| `eventType` | string | The event type being watched |
|
||||
| `topicName` | string | The Cloud Pub/Sub topic |
|
||||
| `createTime` | string | When the watch was created |
|
||||
| `expireTime` | string | When the watch expires \(7 days after creation\) |
|
||||
| `state` | string | The watch state \(ACTIVE, SUSPENDED\) |
|
||||
|
||||
### `google_forms_list_watches`
|
||||
|
||||
List all notification watches for a form
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `watches` | array | List of watches for the form |
|
||||
| ↳ `id` | string | Watch ID |
|
||||
| ↳ `eventType` | string | Event type \(SCHEMA or RESPONSES\) |
|
||||
| ↳ `createTime` | string | When the watch was created |
|
||||
| ↳ `expireTime` | string | When the watch expires |
|
||||
| ↳ `state` | string | Watch state |
|
||||
|
||||
### `google_forms_delete_watch`
|
||||
|
||||
Delete a notification watch from a form
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form |
|
||||
| `watchId` | string | Yes | The ID of the watch to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the watch was successfully deleted |
|
||||
|
||||
### `google_forms_renew_watch`
|
||||
|
||||
Renew a notification watch for another 7 days
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `formId` | string | Yes | The ID of the Google Form |
|
||||
| `watchId` | string | Yes | The ID of the watch to renew |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | The watch ID |
|
||||
| `eventType` | string | The event type being watched |
|
||||
| `expireTime` | string | The new expiration time |
|
||||
| `state` | string | The watch state |
|
||||
|
||||
|
||||
|
||||
@@ -215,4 +215,191 @@ Check if a user is a member of a Google Group
|
||||
| --------- | ---- | ----------- |
|
||||
| `isMember` | boolean | Whether the user is a member of the group |
|
||||
|
||||
### `google_groups_list_aliases`
|
||||
|
||||
List all email aliases for a Google Group
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupKey` | string | Yes | Group email address or unique group ID |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `aliases` | array | List of email aliases for the group |
|
||||
| ↳ `id` | string | Unique group identifier |
|
||||
| ↳ `primaryEmail` | string | Group |
|
||||
| ↳ `alias` | string | Alias email address |
|
||||
| ↳ `kind` | string | API resource type |
|
||||
| ↳ `etag` | string | Resource version identifier |
|
||||
|
||||
### `google_groups_add_alias`
|
||||
|
||||
Add an email alias to a Google Group
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupKey` | string | Yes | Group email address or unique group ID |
|
||||
| `alias` | string | Yes | The email alias to add to the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Unique group identifier |
|
||||
| `primaryEmail` | string | Group |
|
||||
| `alias` | string | The alias that was added |
|
||||
| `kind` | string | API resource type |
|
||||
| `etag` | string | Resource version identifier |
|
||||
|
||||
### `google_groups_remove_alias`
|
||||
|
||||
Remove an email alias from a Google Group
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupKey` | string | Yes | Group email address or unique group ID |
|
||||
| `alias` | string | Yes | The email alias to remove from the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the alias was successfully deleted |
|
||||
|
||||
### `google_groups_get_settings`
|
||||
|
||||
Get the settings for a Google Group including access permissions, moderation, and posting options
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupEmail` | string | Yes | The email address of the group |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `email` | string | The group |
|
||||
| `name` | string | The group name \(max 75 characters\) |
|
||||
| `description` | string | The group description \(max 4096 characters\) |
|
||||
| `whoCanJoin` | string | Who can join the group \(ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN\) |
|
||||
| `whoCanViewMembership` | string | Who can view group membership |
|
||||
| `whoCanViewGroup` | string | Who can view group messages |
|
||||
| `whoCanPostMessage` | string | Who can post messages to the group |
|
||||
| `allowExternalMembers` | string | Whether external users can be members |
|
||||
| `allowWebPosting` | string | Whether web posting is allowed |
|
||||
| `primaryLanguage` | string | The group |
|
||||
| `isArchived` | string | Whether messages are archived |
|
||||
| `archiveOnly` | string | Whether the group is archive-only \(inactive\) |
|
||||
| `messageModerationLevel` | string | Message moderation level |
|
||||
| `spamModerationLevel` | string | Spam handling level \(ALLOW, MODERATE, SILENTLY_MODERATE, REJECT\) |
|
||||
| `replyTo` | string | Default reply destination |
|
||||
| `customReplyTo` | string | Custom email for replies |
|
||||
| `includeCustomFooter` | string | Whether to include custom footer |
|
||||
| `customFooterText` | string | Custom footer text \(max 1000 characters\) |
|
||||
| `sendMessageDenyNotification` | string | Whether to send rejection notifications |
|
||||
| `defaultMessageDenyNotificationText` | string | Default rejection message text |
|
||||
| `membersCanPostAsTheGroup` | string | Whether members can post as the group |
|
||||
| `includeInGlobalAddressList` | string | Whether included in Global Address List |
|
||||
| `whoCanLeaveGroup` | string | Who can leave the group |
|
||||
| `whoCanContactOwner` | string | Who can contact the group owner |
|
||||
| `favoriteRepliesOnTop` | string | Whether favorite replies appear at top |
|
||||
| `whoCanApproveMembers` | string | Who can approve new members |
|
||||
| `whoCanBanUsers` | string | Who can ban users |
|
||||
| `whoCanModerateMembers` | string | Who can manage members |
|
||||
| `whoCanModerateContent` | string | Who can moderate content |
|
||||
| `whoCanAssistContent` | string | Who can assist with content metadata |
|
||||
| `enableCollaborativeInbox` | string | Whether collaborative inbox is enabled |
|
||||
| `whoCanDiscoverGroup` | string | Who can discover the group |
|
||||
| `defaultSender` | string | Default sender identity \(DEFAULT_SELF or GROUP\) |
|
||||
|
||||
### `google_groups_update_settings`
|
||||
|
||||
Update the settings for a Google Group including access permissions, moderation, and posting options
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `groupEmail` | string | Yes | The email address of the group |
|
||||
| `name` | string | No | The group name \(max 75 characters\) |
|
||||
| `description` | string | No | The group description \(max 4096 characters\) |
|
||||
| `whoCanJoin` | string | No | Who can join: ANYONE_CAN_JOIN, ALL_IN_DOMAIN_CAN_JOIN, INVITED_CAN_JOIN, CAN_REQUEST_TO_JOIN |
|
||||
| `whoCanViewMembership` | string | No | Who can view membership: ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW |
|
||||
| `whoCanViewGroup` | string | No | Who can view group messages: ANYONE_CAN_VIEW, ALL_IN_DOMAIN_CAN_VIEW, ALL_MEMBERS_CAN_VIEW, ALL_MANAGERS_CAN_VIEW |
|
||||
| `whoCanPostMessage` | string | No | Who can post: NONE_CAN_POST, ALL_MANAGERS_CAN_POST, ALL_MEMBERS_CAN_POST, ALL_OWNERS_CAN_POST, ALL_IN_DOMAIN_CAN_POST, ANYONE_CAN_POST |
|
||||
| `allowExternalMembers` | string | No | Whether external users can be members: true or false |
|
||||
| `allowWebPosting` | string | No | Whether web posting is allowed: true or false |
|
||||
| `primaryLanguage` | string | No | The group's primary language \(e.g., en\) |
|
||||
| `isArchived` | string | No | Whether messages are archived: true or false |
|
||||
| `archiveOnly` | string | No | Whether the group is archive-only \(inactive\): true or false |
|
||||
| `messageModerationLevel` | string | No | Message moderation: MODERATE_ALL_MESSAGES, MODERATE_NON_MEMBERS, MODERATE_NEW_MEMBERS, MODERATE_NONE |
|
||||
| `spamModerationLevel` | string | No | Spam handling: ALLOW, MODERATE, SILENTLY_MODERATE, REJECT |
|
||||
| `replyTo` | string | No | Default reply: REPLY_TO_CUSTOM, REPLY_TO_SENDER, REPLY_TO_LIST, REPLY_TO_OWNER, REPLY_TO_IGNORE, REPLY_TO_MANAGERS |
|
||||
| `customReplyTo` | string | No | Custom email for replies \(when replyTo is REPLY_TO_CUSTOM\) |
|
||||
| `includeCustomFooter` | string | No | Whether to include custom footer: true or false |
|
||||
| `customFooterText` | string | No | Custom footer text \(max 1000 characters\) |
|
||||
| `sendMessageDenyNotification` | string | No | Whether to send rejection notifications: true or false |
|
||||
| `defaultMessageDenyNotificationText` | string | No | Default rejection message text |
|
||||
| `membersCanPostAsTheGroup` | string | No | Whether members can post as the group: true or false |
|
||||
| `includeInGlobalAddressList` | string | No | Whether included in Global Address List: true or false |
|
||||
| `whoCanLeaveGroup` | string | No | Who can leave: ALL_MANAGERS_CAN_LEAVE, ALL_MEMBERS_CAN_LEAVE, NONE_CAN_LEAVE |
|
||||
| `whoCanContactOwner` | string | No | Who can contact owner: ALL_IN_DOMAIN_CAN_CONTACT, ALL_MANAGERS_CAN_CONTACT, ALL_MEMBERS_CAN_CONTACT, ANYONE_CAN_CONTACT |
|
||||
| `favoriteRepliesOnTop` | string | No | Whether favorite replies appear at top: true or false |
|
||||
| `whoCanApproveMembers` | string | No | Who can approve members: ALL_OWNERS_CAN_APPROVE, ALL_MANAGERS_CAN_APPROVE, ALL_MEMBERS_CAN_APPROVE, NONE_CAN_APPROVE |
|
||||
| `whoCanBanUsers` | string | No | Who can ban users: OWNERS_ONLY, OWNERS_AND_MANAGERS, NONE |
|
||||
| `whoCanModerateMembers` | string | No | Who can manage members: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
|
||||
| `whoCanModerateContent` | string | No | Who can moderate content: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
|
||||
| `whoCanAssistContent` | string | No | Who can assist with content metadata: OWNERS_ONLY, OWNERS_AND_MANAGERS, ALL_MEMBERS, NONE |
|
||||
| `enableCollaborativeInbox` | string | No | Whether collaborative inbox is enabled: true or false |
|
||||
| `whoCanDiscoverGroup` | string | No | Who can discover: ANYONE_CAN_DISCOVER, ALL_IN_DOMAIN_CAN_DISCOVER, ALL_MEMBERS_CAN_DISCOVER |
|
||||
| `defaultSender` | string | No | Default sender: DEFAULT_SELF or GROUP |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `email` | string | The group |
|
||||
| `name` | string | The group name |
|
||||
| `description` | string | The group description |
|
||||
| `whoCanJoin` | string | Who can join the group |
|
||||
| `whoCanViewMembership` | string | Who can view group membership |
|
||||
| `whoCanViewGroup` | string | Who can view group messages |
|
||||
| `whoCanPostMessage` | string | Who can post messages to the group |
|
||||
| `allowExternalMembers` | string | Whether external users can be members |
|
||||
| `allowWebPosting` | string | Whether web posting is allowed |
|
||||
| `primaryLanguage` | string | The group |
|
||||
| `isArchived` | string | Whether messages are archived |
|
||||
| `archiveOnly` | string | Whether the group is archive-only |
|
||||
| `messageModerationLevel` | string | Message moderation level |
|
||||
| `spamModerationLevel` | string | Spam handling level |
|
||||
| `replyTo` | string | Default reply destination |
|
||||
| `customReplyTo` | string | Custom email for replies |
|
||||
| `includeCustomFooter` | string | Whether to include custom footer |
|
||||
| `customFooterText` | string | Custom footer text |
|
||||
| `sendMessageDenyNotification` | string | Whether to send rejection notifications |
|
||||
| `defaultMessageDenyNotificationText` | string | Default rejection message text |
|
||||
| `membersCanPostAsTheGroup` | string | Whether members can post as the group |
|
||||
| `includeInGlobalAddressList` | string | Whether included in Global Address List |
|
||||
| `whoCanLeaveGroup` | string | Who can leave the group |
|
||||
| `whoCanContactOwner` | string | Who can contact the group owner |
|
||||
| `favoriteRepliesOnTop` | string | Whether favorite replies appear at top |
|
||||
| `whoCanApproveMembers` | string | Who can approve new members |
|
||||
| `whoCanBanUsers` | string | Who can ban users |
|
||||
| `whoCanModerateMembers` | string | Who can manage members |
|
||||
| `whoCanModerateContent` | string | Who can moderate content |
|
||||
| `whoCanAssistContent` | string | Who can assist with content metadata |
|
||||
| `enableCollaborativeInbox` | string | Whether collaborative inbox is enabled |
|
||||
| `whoCanDiscoverGroup` | string | Who can discover the group |
|
||||
| `defaultSender` | string | Default sender identity |
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ In Sim, the Google Sheets integration empowers your agents to automate reading f
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, and update data in specific sheets.
|
||||
Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, update, clear data, create spreadsheets, get spreadsheet info, and copy sheets.
|
||||
|
||||
|
||||
|
||||
@@ -42,9 +42,8 @@ Read data from a specific sheet in a Google Sheets spreadsheet
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to read from |
|
||||
| `cellRange` | string | No | The cell range to read \(e.g. "A1:D10"\). Defaults to "A1:Z1000" if not specified. |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet \(found in the URL: docs.google.com/spreadsheets/d/\{SPREADSHEET_ID\}/edit\). |
|
||||
| `range` | string | No | The A1 notation range to read \(e.g. "Sheet1!A1:D10", "A1:B5"\). Defaults to first sheet A1:Z1000 if not specified. |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -66,8 +65,7 @@ Write data to a specific sheet in a Google Sheets spreadsheet
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to write to |
|
||||
| `cellRange` | string | No | The cell range to write to \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. |
|
||||
| `range` | string | No | The A1 notation range to write to \(e.g. "Sheet1!A1:D10", "A1:B5"\) |
|
||||
| `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. |
|
||||
| `valueInputOption` | string | No | The format of the data to write |
|
||||
| `includeValuesInResponse` | boolean | No | Whether to include the written values in the response |
|
||||
@@ -93,8 +91,7 @@ Update data in a specific sheet in a Google Sheets spreadsheet
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to update |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to update |
|
||||
| `cellRange` | string | No | The cell range to update \(e.g. "A1:D10", "A1"\). Defaults to "A1" if not specified. |
|
||||
| `range` | string | No | The A1 notation range to update \(e.g. "Sheet1!A1:D10", "A1:B5"\) |
|
||||
| `values` | array | Yes | The data to update as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects. |
|
||||
| `valueInputOption` | string | No | The format of the data to update |
|
||||
| `includeValuesInResponse` | boolean | No | Whether to include the updated values in the response |
|
||||
@@ -120,7 +117,7 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to append to |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to append to |
|
||||
| `range` | string | No | The A1 notation range to append after \(e.g. "Sheet1", "Sheet1!A:D"\) |
|
||||
| `values` | array | Yes | The data to append as a 2D array \(e.g. \[\["Alice", 30\], \["Bob", 25\]\]\) or array of objects. |
|
||||
| `valueInputOption` | string | No | The format of the data to append |
|
||||
| `insertDataOption` | string | No | How to insert the data \(OVERWRITE or INSERT_ROWS\) |
|
||||
@@ -139,4 +136,180 @@ Append data to the end of a specific sheet in a Google Sheets spreadsheet
|
||||
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
|
||||
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
|
||||
|
||||
### `google_sheets_clear`
|
||||
|
||||
Clear values from a specific range in a Google Sheets spreadsheet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to clear |
|
||||
| `cellRange` | string | No | The cell range to clear \(e.g. "A1:D10"\). Clears entire sheet if not specified. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `clearedRange` | string | The range that was cleared |
|
||||
| `sheetName` | string | Name of the sheet that was cleared |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
|
||||
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
|
||||
|
||||
### `google_sheets_get_spreadsheet`
|
||||
|
||||
Get metadata about a Google Sheets spreadsheet including title and sheet list
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `includeGridData` | boolean | No | Whether to include grid data \(cell values\). Defaults to false. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `spreadsheetId` | string | The spreadsheet ID |
|
||||
| `title` | string | The title of the spreadsheet |
|
||||
| `locale` | string | The locale of the spreadsheet |
|
||||
| `timeZone` | string | The time zone of the spreadsheet |
|
||||
| `spreadsheetUrl` | string | URL to the spreadsheet |
|
||||
| `sheets` | array | List of sheets in the spreadsheet |
|
||||
| ↳ `sheetId` | number | The sheet ID |
|
||||
| ↳ `title` | string | The sheet title/name |
|
||||
| ↳ `index` | number | The sheet index \(position\) |
|
||||
| ↳ `rowCount` | number | Number of rows in the sheet |
|
||||
| ↳ `columnCount` | number | Number of columns in the sheet |
|
||||
| ↳ `hidden` | boolean | Whether the sheet is hidden |
|
||||
|
||||
### `google_sheets_create_spreadsheet`
|
||||
|
||||
Create a new Google Sheets spreadsheet
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `title` | string | Yes | The title of the new spreadsheet |
|
||||
| `sheetTitles` | json | No | Array of sheet names to create \(e.g., \["Sheet1", "Data", "Summary"\]\). Defaults to a single "Sheet1". |
|
||||
| `locale` | string | No | The locale of the spreadsheet \(e.g., "en_US"\) |
|
||||
| `timeZone` | string | No | The time zone of the spreadsheet \(e.g., "America/New_York"\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `spreadsheetId` | string | The ID of the created spreadsheet |
|
||||
| `title` | string | The title of the created spreadsheet |
|
||||
| `spreadsheetUrl` | string | URL to the created spreadsheet |
|
||||
| `sheets` | array | List of sheets created in the spreadsheet |
|
||||
| ↳ `sheetId` | number | The sheet ID |
|
||||
| ↳ `title` | string | The sheet title/name |
|
||||
| ↳ `index` | number | The sheet index \(position\) |
|
||||
|
||||
### `google_sheets_batch_get`
|
||||
|
||||
Read multiple ranges from a Google Sheets spreadsheet in a single request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `ranges` | json | Yes | Array of ranges to read \(e.g., \["Sheet1!A1:D10", "Sheet2!A1:B5"\]\). Each range should include sheet name. |
|
||||
| `majorDimension` | string | No | The major dimension of values: "ROWS" \(default\) or "COLUMNS" |
|
||||
| `valueRenderOption` | string | No | How values should be rendered: "FORMATTED_VALUE" \(default\), "UNFORMATTED_VALUE", or "FORMULA" |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `spreadsheetId` | string | The spreadsheet ID |
|
||||
| `valueRanges` | array | Array of value ranges read from the spreadsheet |
|
||||
| ↳ `range` | string | The range that was read |
|
||||
| ↳ `majorDimension` | string | Major dimension \(ROWS or COLUMNS\) |
|
||||
| ↳ `values` | array | The cell values as a 2D array |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
|
||||
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
|
||||
|
||||
### `google_sheets_batch_update`
|
||||
|
||||
Update multiple ranges in a Google Sheets spreadsheet in a single request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `data` | json | Yes | Array of value ranges to update. Each item should have "range" \(e.g., "Sheet1!A1:D10"\) and "values" \(2D array\). |
|
||||
| `valueInputOption` | string | No | How input data should be interpreted: "RAW" or "USER_ENTERED" \(default\). USER_ENTERED parses formulas. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `spreadsheetId` | string | The spreadsheet ID |
|
||||
| `totalUpdatedRows` | number | Total number of rows updated |
|
||||
| `totalUpdatedColumns` | number | Total number of columns updated |
|
||||
| `totalUpdatedCells` | number | Total number of cells updated |
|
||||
| `totalUpdatedSheets` | number | Total number of sheets updated |
|
||||
| `responses` | array | Array of update responses for each range |
|
||||
| ↳ `spreadsheetId` | string | The spreadsheet ID |
|
||||
| ↳ `updatedRange` | string | The range that was updated |
|
||||
| ↳ `updatedRows` | number | Number of rows updated in this range |
|
||||
| ↳ `updatedColumns` | number | Number of columns updated in this range |
|
||||
| ↳ `updatedCells` | number | Number of cells updated in this range |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
|
||||
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
|
||||
|
||||
### `google_sheets_batch_clear`
|
||||
|
||||
Clear multiple ranges in a Google Sheets spreadsheet in a single request
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet |
|
||||
| `ranges` | json | Yes | Array of ranges to clear \(e.g., \["Sheet1!A1:D10", "Sheet2!A1:B5"\]\). Each range should include sheet name. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `spreadsheetId` | string | The spreadsheet ID |
|
||||
| `clearedRanges` | array | Array of ranges that were cleared |
|
||||
| `metadata` | json | Spreadsheet metadata including ID and URL |
|
||||
| ↳ `spreadsheetId` | string | Google Sheets spreadsheet ID |
|
||||
| ↳ `spreadsheetUrl` | string | Spreadsheet URL |
|
||||
|
||||
### `google_sheets_copy_sheet`
|
||||
|
||||
Copy a sheet from one spreadsheet to another
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `sourceSpreadsheetId` | string | Yes | The ID of the source spreadsheet |
|
||||
| `sheetId` | number | Yes | The ID of the sheet to copy \(numeric ID, not the sheet name\). Use Get Spreadsheet to find sheet IDs. |
|
||||
| `destinationSpreadsheetId` | string | Yes | The ID of the destination spreadsheet where the sheet will be copied |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sheetId` | number | The ID of the newly created sheet in the destination |
|
||||
| `title` | string | The title of the copied sheet |
|
||||
| `index` | number | The index \(position\) of the copied sheet |
|
||||
| `sheetType` | string | The type of the sheet \(GRID, CHART, etc.\) |
|
||||
| `destinationSpreadsheetId` | string | The ID of the destination spreadsheet |
|
||||
| `destinationSpreadsheetUrl` | string | URL to the destination spreadsheet |
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ In Sim, the Google Slides integration enables your agents to interact directly w
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails.
|
||||
Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text.
|
||||
|
||||
|
||||
|
||||
@@ -177,4 +177,157 @@ Generate a thumbnail image of a specific slide in a Google Slides presentation
|
||||
| `height` | number | Height of the thumbnail in pixels |
|
||||
| `metadata` | json | Operation metadata including presentation ID and page object ID |
|
||||
|
||||
### `google_slides_get_page`
|
||||
|
||||
Get detailed information about a specific slide/page in a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `pageObjectId` | string | Yes | The object ID of the slide/page to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `objectId` | string | The object ID of the page |
|
||||
| `pageType` | string | The type of page \(SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER\) |
|
||||
| `pageElements` | json | Array of page elements \(shapes, images, tables, etc.\) on this page |
|
||||
| `slideProperties` | json | Properties specific to slides \(layout, master, notes\) |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
|
||||
### `google_slides_delete_object`
|
||||
|
||||
Delete a page element (shape, image, table, etc.) or an entire slide from a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `objectId` | string | Yes | The object ID of the element or slide to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the object was successfully deleted |
|
||||
| `objectId` | string | The object ID that was deleted |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
|
||||
### `google_slides_duplicate_object`
|
||||
|
||||
Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `objectId` | string | Yes | The object ID of the element or slide to duplicate |
|
||||
| `objectIds` | string | No | Optional JSON object mapping source object IDs \(within the slide being duplicated\) to new object IDs for the duplicates. Format: \{"sourceId1":"newId1","sourceId2":"newId2"\} |
|
||||
| `Format` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `duplicatedObjectId` | string | The object ID of the newly created duplicate |
|
||||
| `metadata` | json | Operation metadata including presentation ID and source object ID |
|
||||
|
||||
### `google_slides_update_slides_position`
|
||||
|
||||
Move one or more slides to a new position in a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `slideObjectIds` | string | Yes | Comma-separated list of slide object IDs to move. The slides will maintain their relative order. |
|
||||
| `insertionIndex` | number | Yes | The zero-based index where the slides should be moved. All slides with indices greater than or equal to this will be shifted right. |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `moved` | boolean | Whether the slides were successfully moved |
|
||||
| `slideObjectIds` | array | The slide object IDs that were moved |
|
||||
| `insertionIndex` | number | The index where the slides were moved to |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
|
||||
### `google_slides_create_table`
|
||||
|
||||
Create a new table on a slide in a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `pageObjectId` | string | Yes | The object ID of the slide/page to add the table to |
|
||||
| `rows` | number | Yes | Number of rows in the table \(minimum 1\) |
|
||||
| `columns` | number | Yes | Number of columns in the table \(minimum 1\) |
|
||||
| `width` | number | No | Width of the table in points \(default: 400\) |
|
||||
| `height` | number | No | Height of the table in points \(default: 200\) |
|
||||
| `positionX` | number | No | X position from the left edge in points \(default: 100\) |
|
||||
| `positionY` | number | No | Y position from the top edge in points \(default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `tableId` | string | The object ID of the newly created table |
|
||||
| `rows` | number | Number of rows in the table |
|
||||
| `columns` | number | Number of columns in the table |
|
||||
| `metadata` | json | Operation metadata including presentation ID and page object ID |
|
||||
|
||||
### `google_slides_create_shape`
|
||||
|
||||
Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Google Slides presentation
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `pageObjectId` | string | Yes | The object ID of the slide/page to add the shape to |
|
||||
| `shapeType` | string | Yes | The type of shape to create. Common types: TEXT_BOX, RECTANGLE, ROUND_RECTANGLE, ELLIPSE, TRIANGLE, DIAMOND, STAR_5, ARROW_EAST, HEART, CLOUD |
|
||||
| `width` | number | No | Width of the shape in points \(default: 200\) |
|
||||
| `height` | number | No | Height of the shape in points \(default: 100\) |
|
||||
| `positionX` | number | No | X position from the left edge in points \(default: 100\) |
|
||||
| `positionY` | number | No | Y position from the top edge in points \(default: 100\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `shapeId` | string | The object ID of the newly created shape |
|
||||
| `shapeType` | string | The type of shape that was created |
|
||||
| `metadata` | json | Operation metadata including presentation ID and page object ID |
|
||||
|
||||
### `google_slides_insert_text`
|
||||
|
||||
Insert text into a shape or table cell in a Google Slides presentation. Use this to add text to text boxes, shapes, or table cells.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `presentationId` | string | Yes | The ID of the presentation |
|
||||
| `objectId` | string | Yes | The object ID of the shape or table cell to insert text into. For table cells, use the cell object ID. |
|
||||
| `text` | string | Yes | The text to insert |
|
||||
| `insertionIndex` | number | No | The zero-based index at which to insert the text. If not specified, text is inserted at the beginning \(index 0\). |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `inserted` | boolean | Whether the text was successfully inserted |
|
||||
| `objectId` | string | The object ID where text was inserted |
|
||||
| `text` | string | The text that was inserted |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ Search for similar content in a knowledge base using vector similarity
|
||||
| `properties` | string | No | No description |
|
||||
| `tagName` | string | No | No description |
|
||||
| `tagValue` | string | No | No description |
|
||||
| `tagFilters` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -108,19 +109,8 @@ Create a new document in a knowledge base
|
||||
| `knowledgeBaseId` | string | Yes | ID of the knowledge base containing the document |
|
||||
| `name` | string | Yes | Name of the document |
|
||||
| `content` | string | Yes | Content of the document |
|
||||
| `tag1` | string | No | Tag 1 value for the document |
|
||||
| `tag2` | string | No | Tag 2 value for the document |
|
||||
| `tag3` | string | No | Tag 3 value for the document |
|
||||
| `tag4` | string | No | Tag 4 value for the document |
|
||||
| `tag5` | string | No | Tag 5 value for the document |
|
||||
| `tag6` | string | No | Tag 6 value for the document |
|
||||
| `tag7` | string | No | Tag 7 value for the document |
|
||||
| `documentTagsData` | array | No | Structured tag data with names, types, and values |
|
||||
| `items` | object | No | No description |
|
||||
| `properties` | string | No | No description |
|
||||
| `tagName` | string | No | No description |
|
||||
| `tagValue` | string | No | No description |
|
||||
| `tagType` | string | No | No description |
|
||||
| `documentTags` | object | No | Document tags |
|
||||
| `documentTags` | string | No | No description |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -45,8 +45,7 @@ Read data from a specific sheet in a Microsoft Excel spreadsheet
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to read from |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to read from |
|
||||
| `cellRange` | string | No | The cell range to read \(e.g., "A1:D10"\). If not specified, reads the entire used range. |
|
||||
| `range` | string | No | The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet. |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -68,9 +67,8 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `spreadsheetId` | string | Yes | The ID of the spreadsheet to write to |
|
||||
| `sheetName` | string | Yes | The name of the sheet/tab to write to |
|
||||
| `cellRange` | string | No | The cell range to write to \(e.g., "A1:D10", "A1"\). Defaults to "A1" if not specified. |
|
||||
| `values` | array | Yes | The data to write as a 2D array \(e.g. \[\["Name", "Age"\], \["Alice", 30\], \["Bob", 25\]\]\) or array of objects. |
|
||||
| `range` | string | No | The range of cells to write to |
|
||||
| `values` | array | Yes | The data to write to the spreadsheet |
|
||||
| `valueInputOption` | string | No | The format of the data to write |
|
||||
| `includeValuesInResponse` | boolean | No | Whether to include the written values in the response |
|
||||
|
||||
|
||||
@@ -84,9 +84,10 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `destinationType` | string | No | Destination type: channel or dm |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | No | Target Slack channel \(e.g., #general\) |
|
||||
| `userId` | string | No | Target Slack user ID for direct messages \(e.g., U1234567890\) |
|
||||
| `dmUserId` | string | No | Target Slack user for direct messages |
|
||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
|
||||
| `files` | file[] | No | Files to attach to the message |
|
||||
@@ -132,9 +133,10 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `authMethod` | string | No | Authentication method: oauth or bot_token |
|
||||
| `destinationType` | string | No | Destination type: channel or dm |
|
||||
| `botToken` | string | No | Bot token for Custom Bot |
|
||||
| `channel` | string | No | Slack channel to read messages from \(e.g., #general\) |
|
||||
| `userId` | string | No | User ID for DM conversation \(e.g., U1234567890\) |
|
||||
| `dmUserId` | string | No | Target Slack user for DM conversation |
|
||||
| `limit` | number | No | Number of messages to retrieve \(default: 10, max: 15\) |
|
||||
| `oldest` | string | No | Start of time range \(timestamp\) |
|
||||
| `latest` | string | No | End of time range \(timestamp\) |
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
||||
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
||||
@@ -19,19 +19,32 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
try {
|
||||
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||
if (!auth.success) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
// Only allow session and internal JWT auth (not API key)
|
||||
if (auth.authType === 'api_key') {
|
||||
return NextResponse.json(
|
||||
{ error: 'API key auth not supported for this endpoint' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const tagDefinitions = await getTagDefinitions(knowledgeBaseId)
|
||||
|
||||
logger.info(`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions`)
|
||||
logger.info(
|
||||
`[${requestId}] Retrieved ${tagDefinitions.length} tag definitions (${auth.authType})`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -51,14 +64,25 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
try {
|
||||
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
||||
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
||||
if (!auth.success) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, session.user.id)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
// Only allow session and internal JWT auth (not API key)
|
||||
if (auth.authType === 'api_key') {
|
||||
return NextResponse.json(
|
||||
{ error: 'API key auth not supported for this endpoint' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// For session auth, verify KB access. Internal JWT is trusted.
|
||||
if (auth.authType === 'session' && auth.userId) {
|
||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||
if (!accessCheck.hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type HTMLAttributes, type ReactNode } from 'react'
|
||||
import React, { type HTMLAttributes, memo, type ReactNode, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
@@ -23,24 +23,16 @@ export function LinkWithPreview({ href, children }: { href: string; children: Re
|
||||
)
|
||||
}
|
||||
|
||||
export default function MarkdownRenderer({
|
||||
content,
|
||||
customLinkComponent,
|
||||
}: {
|
||||
content: string
|
||||
customLinkComponent?: typeof LinkWithPreview
|
||||
}) {
|
||||
const LinkComponent = customLinkComponent || LinkWithPreview
|
||||
const REMARK_PLUGINS = [remarkGfm]
|
||||
|
||||
const customComponents = {
|
||||
// Paragraph
|
||||
function createCustomComponents(LinkComponent: typeof LinkWithPreview) {
|
||||
return {
|
||||
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className='mb-1 font-sans text-base text-gray-800 leading-relaxed last:mb-0 dark:text-gray-200'>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
|
||||
// Headings
|
||||
h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h1 className='mt-10 mb-5 font-sans font-semibold text-2xl text-gray-900 dark:text-gray-100'>
|
||||
{children}
|
||||
@@ -62,7 +54,6 @@ export default function MarkdownRenderer({
|
||||
</h4>
|
||||
),
|
||||
|
||||
// Lists
|
||||
ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul
|
||||
className='mt-1 mb-1 space-y-1 pl-6 font-sans text-gray-800 dark:text-gray-200'
|
||||
@@ -89,7 +80,6 @@ export default function MarkdownRenderer({
|
||||
</li>
|
||||
),
|
||||
|
||||
// Code blocks
|
||||
pre: ({ children }: HTMLAttributes<HTMLPreElement>) => {
|
||||
let codeProps: HTMLAttributes<HTMLElement> = {}
|
||||
let codeContent: ReactNode = children
|
||||
@@ -120,7 +110,6 @@ export default function MarkdownRenderer({
|
||||
)
|
||||
},
|
||||
|
||||
// Inline code
|
||||
code: ({
|
||||
inline,
|
||||
className,
|
||||
@@ -144,24 +133,20 @@ export default function MarkdownRenderer({
|
||||
)
|
||||
},
|
||||
|
||||
// Blockquotes
|
||||
blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
|
||||
<blockquote className='my-4 border-gray-300 border-l-4 py-1 pl-4 font-sans text-gray-700 italic dark:border-gray-600 dark:text-gray-300'>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// Horizontal rule
|
||||
hr: () => <hr className='my-8 border-gray-500/[.07] border-t dark:border-gray-400/[.07]' />,
|
||||
|
||||
// Links
|
||||
a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<LinkComponent href={href || '#'} {...props}>
|
||||
{children}
|
||||
</LinkComponent>
|
||||
),
|
||||
|
||||
// Tables
|
||||
table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
|
||||
<div className='my-4 w-full overflow-x-auto'>
|
||||
<table className='min-w-full table-auto border border-gray-300 font-sans text-sm dark:border-gray-700'>
|
||||
@@ -193,7 +178,6 @@ export default function MarkdownRenderer({
|
||||
</td>
|
||||
),
|
||||
|
||||
// Images
|
||||
img: ({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
<img
|
||||
src={src}
|
||||
@@ -203,15 +187,33 @@ export default function MarkdownRenderer({
|
||||
/>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_COMPONENTS = createCustomComponents(LinkWithPreview)
|
||||
|
||||
const MarkdownRenderer = memo(function MarkdownRenderer({
|
||||
content,
|
||||
customLinkComponent,
|
||||
}: {
|
||||
content: string
|
||||
customLinkComponent?: typeof LinkWithPreview
|
||||
}) {
|
||||
const components = useMemo(() => {
|
||||
if (!customLinkComponent) {
|
||||
return DEFAULT_COMPONENTS
|
||||
}
|
||||
return createCustomComponents(customLinkComponent)
|
||||
}, [customLinkComponent])
|
||||
|
||||
// Pre-process content to fix common issues
|
||||
const processedContent = content.trim()
|
||||
|
||||
return (
|
||||
<div className='space-y-4 break-words font-sans text-[#0D0D0D] text-base leading-relaxed dark:text-gray-100'>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={customComponents}>
|
||||
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={components}>
|
||||
{processedContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default MarkdownRenderer
|
||||
|
||||
@@ -7,7 +7,7 @@ import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/
|
||||
import { PostHogProvider } from '@/app/_shell/providers/posthog-provider'
|
||||
import '@/app/_styles/globals.css'
|
||||
import { OneDollarStats } from '@/components/analytics/onedollarstats'
|
||||
import { isReactGrabEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/feature-flags'
|
||||
import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler'
|
||||
import { QueryProvider } from '@/app/_shell/providers/query-provider'
|
||||
import { SessionProvider } from '@/app/_shell/providers/session-provider'
|
||||
@@ -35,6 +35,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
return (
|
||||
<html lang='en' suppressHydrationWarning>
|
||||
<head>
|
||||
{isReactScanEnabled && (
|
||||
<Script
|
||||
src='https://unpkg.com/react-scan/dist/auto.global.js'
|
||||
crossOrigin='anonymous'
|
||||
strategy='beforeInteractive'
|
||||
/>
|
||||
)}
|
||||
{isReactGrabEnabled && (
|
||||
<Script
|
||||
src='https://unpkg.com/react-grab/dist/index.global.js'
|
||||
|
||||
@@ -4,13 +4,13 @@ import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useUserPermissions, type WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
import {
|
||||
useWorkspacePermissions,
|
||||
type WorkspacePermissions,
|
||||
} from '@/hooks/use-workspace-permissions'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
|
||||
const logger = createLogger('WorkspacePermissionsProvider')
|
||||
|
||||
@@ -64,8 +64,8 @@ export function WorkspacePermissionsProvider({ children }: WorkspacePermissionsP
|
||||
// Track whether we've already surfaced an offline notification to avoid duplicates
|
||||
const [hasShownOfflineNotification, setHasShownOfflineNotification] = useState(false)
|
||||
|
||||
// Get operation error state from collaborative workflow
|
||||
const { hasOperationError } = useCollaborativeWorkflow()
|
||||
// Get operation error state directly from the store (avoid full useCollaborativeWorkflow subscription)
|
||||
const hasOperationError = useOperationQueueStore((state) => state.hasOperationError)
|
||||
|
||||
const addNotification = useNotificationStore((state) => state.addNotification)
|
||||
|
||||
|
||||
@@ -48,17 +48,17 @@ export const ActionBar = memo(
|
||||
collaborativeBatchToggleBlockEnabled,
|
||||
collaborativeBatchToggleBlockHandles,
|
||||
} = useCollaborativeWorkflow()
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const subBlockStore = useSubBlockStore()
|
||||
const { activeWorkflowId, setPendingSelection } = useWorkflowRegistry()
|
||||
|
||||
const handleDuplicateBlock = useCallback(() => {
|
||||
const blocks = useWorkflowStore.getState().blocks
|
||||
const sourceBlock = blocks[blockId]
|
||||
if (!sourceBlock) return
|
||||
|
||||
const newId = crypto.randomUUID()
|
||||
const newName = getUniqueBlockName(sourceBlock.name, blocks)
|
||||
const subBlockValues = subBlockStore.workflowValues[activeWorkflowId || '']?.[blockId] || {}
|
||||
const subBlockValues =
|
||||
useSubBlockStore.getState().workflowValues[activeWorkflowId || '']?.[blockId] || {}
|
||||
|
||||
const { block, subBlockValues: filteredValues } = prepareDuplicateBlockState({
|
||||
sourceBlock,
|
||||
@@ -68,18 +68,10 @@ export const ActionBar = memo(
|
||||
subBlockValues,
|
||||
})
|
||||
|
||||
setPendingSelection([newId])
|
||||
collaborativeBatchAddBlocks([block], [], {}, {}, { [newId]: filteredValues })
|
||||
}, [
|
||||
blockId,
|
||||
blocks,
|
||||
activeWorkflowId,
|
||||
subBlockStore.workflowValues,
|
||||
collaborativeBatchAddBlocks,
|
||||
])
|
||||
}, [blockId, activeWorkflowId, collaborativeBatchAddBlocks, setPendingSelection])
|
||||
|
||||
/**
|
||||
* Optimized single store subscription for all block data
|
||||
*/
|
||||
const { isEnabled, horizontalHandles, parentId, parentType } = useWorkflowStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
|
||||
@@ -3,13 +3,11 @@ import ReactMarkdown from 'react-markdown'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { BLOCK_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { ActionBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar'
|
||||
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import {
|
||||
BLOCK_DIMENSIONS,
|
||||
useBlockDimensions,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
|
||||
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import type { WorkflowBlockProps } from '../workflow-block/types'
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Code, Tooltip } from '@/components/emcn'
|
||||
|
||||
const REMARK_PLUGINS = [remarkGfm]
|
||||
|
||||
/**
|
||||
* Recursively extracts text content from React elements
|
||||
* @param element - React node to extract text from
|
||||
@@ -149,14 +151,12 @@ interface CopilotMarkdownRendererProps {
|
||||
* Tighter spacing compared to traditional prose for better chat UX
|
||||
*/
|
||||
const markdownComponents = {
|
||||
// Paragraphs - tight spacing, no margin on last
|
||||
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className='mb-1.5 font-base font-season text-[var(--text-primary)] text-sm leading-[1.4] last:mb-0 dark:font-[470]'>
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
|
||||
// Headings - minimal margins for chat context
|
||||
h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h1 className='mt-2 mb-1 font-season font-semibold text-[var(--text-primary)] text-base first:mt-0'>
|
||||
{children}
|
||||
@@ -178,7 +178,6 @@ const markdownComponents = {
|
||||
</h4>
|
||||
),
|
||||
|
||||
// Lists - compact spacing
|
||||
ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul
|
||||
className='my-1 space-y-0.5 pl-5 font-base font-season text-[var(--text-primary)] dark:font-[470]'
|
||||
@@ -204,7 +203,6 @@ const markdownComponents = {
|
||||
</li>
|
||||
),
|
||||
|
||||
// Code blocks - handled by CodeBlock component
|
||||
pre: ({ children }: React.HTMLAttributes<HTMLPreElement>) => {
|
||||
let codeContent: React.ReactNode = children
|
||||
let language = 'code'
|
||||
@@ -243,7 +241,6 @@ const markdownComponents = {
|
||||
return <CodeBlock code={actualCodeText} language={language} />
|
||||
},
|
||||
|
||||
// Inline code
|
||||
code: ({
|
||||
className,
|
||||
children,
|
||||
@@ -257,7 +254,6 @@ const markdownComponents = {
|
||||
</code>
|
||||
),
|
||||
|
||||
// Text formatting
|
||||
strong: ({ children }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<strong className='font-semibold text-[var(--text-primary)]'>{children}</strong>
|
||||
),
|
||||
@@ -271,22 +267,18 @@ const markdownComponents = {
|
||||
<i className='text-[var(--text-primary)] italic'>{children}</i>
|
||||
),
|
||||
|
||||
// Blockquote - compact
|
||||
blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
|
||||
<blockquote className='my-1.5 border-[var(--border-1)] border-l-2 py-0.5 pl-3 font-season text-[var(--text-secondary)] text-sm italic'>
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
|
||||
// Horizontal rule
|
||||
hr: () => <hr className='my-3 border-[var(--divider)] border-t' />,
|
||||
|
||||
// Links
|
||||
a: ({ href, children }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<LinkWithPreview href={href || '#'}>{children}</LinkWithPreview>
|
||||
),
|
||||
|
||||
// Tables - compact
|
||||
table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
|
||||
<div className='my-2 max-w-full overflow-x-auto'>
|
||||
<table className='min-w-full table-auto border border-[var(--border-1)] font-season text-xs'>
|
||||
@@ -314,7 +306,6 @@ const markdownComponents = {
|
||||
</td>
|
||||
),
|
||||
|
||||
// Images
|
||||
img: ({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
<img src={src} alt={alt || 'Image'} className='my-2 h-auto max-w-full rounded-md' {...props} />
|
||||
),
|
||||
@@ -330,7 +321,7 @@ const markdownComponents = {
|
||||
function CopilotMarkdownRenderer({ content }: CopilotMarkdownRendererProps) {
|
||||
return (
|
||||
<div className='max-w-full break-words font-base font-season text-[var(--text-primary)] text-sm leading-[1.4] dark:font-[470] [&_*]:max-w-full [&_a]:break-all [&_code:not(pre_code)]:break-words [&_li]:break-words [&_p]:break-words'>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||
<ReactMarkdown remarkPlugins={REMARK_PLUGINS} components={markdownComponents}>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { useCheckpointManagement } from './use-checkpoint-management'
|
||||
export { useMessageEditing } from './use-message-editing'
|
||||
export { useMessageFeedback } from './use-message-feedback'
|
||||
export { useSuccessTimers } from './use-success-timers'
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { CopilotMessage } from '@/stores/panel'
|
||||
import { useCopilotStore } from '@/stores/panel'
|
||||
|
||||
const logger = createLogger('useMessageFeedback')
|
||||
|
||||
const WORKFLOW_TOOL_NAMES = ['edit_workflow']
|
||||
|
||||
interface UseMessageFeedbackProps {
|
||||
setShowUpvoteSuccess: (show: boolean) => void
|
||||
setShowDownvoteSuccess: (show: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to handle message feedback (upvote/downvote)
|
||||
*
|
||||
* @param message - The copilot message
|
||||
* @param messages - Array of all messages in the chat
|
||||
* @param props - Success state setters from useSuccessTimers
|
||||
* @returns Feedback management utilities
|
||||
*/
|
||||
export function useMessageFeedback(
|
||||
message: CopilotMessage,
|
||||
messages: CopilotMessage[],
|
||||
props: UseMessageFeedbackProps
|
||||
) {
|
||||
const { setShowUpvoteSuccess, setShowDownvoteSuccess } = props
|
||||
const { currentChat } = useCopilotStore()
|
||||
|
||||
/**
|
||||
* Gets the full assistant response content from message
|
||||
*/
|
||||
const getFullAssistantContent = useCallback((message: CopilotMessage) => {
|
||||
if (message.content?.trim()) {
|
||||
return message.content
|
||||
}
|
||||
|
||||
if (message.contentBlocks && message.contentBlocks.length > 0) {
|
||||
return message.contentBlocks
|
||||
.filter((block) => block.type === 'text')
|
||||
.map((block) => block.content)
|
||||
.join('')
|
||||
}
|
||||
|
||||
return message.content || ''
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Finds the last user query before this assistant message
|
||||
*/
|
||||
const getLastUserQuery = useCallback(() => {
|
||||
const messageIndex = messages.findIndex((msg) => msg.id === message.id)
|
||||
if (messageIndex === -1) return null
|
||||
|
||||
for (let i = messageIndex - 1; i >= 0; i--) {
|
||||
if (messages[i].role === 'user') {
|
||||
return messages[i].content
|
||||
}
|
||||
}
|
||||
return null
|
||||
}, [messages, message.id])
|
||||
|
||||
/**
|
||||
* Submits feedback to the API
|
||||
*/
|
||||
const submitFeedback = useCallback(
|
||||
async (isPositive: boolean) => {
|
||||
if (!currentChat?.id) {
|
||||
logger.error('No current chat ID available for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
const userQuery = getLastUserQuery()
|
||||
if (!userQuery) {
|
||||
logger.error('No user query found for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
const agentResponse = getFullAssistantContent(message)
|
||||
if (!agentResponse.trim()) {
|
||||
logger.error('No agent response content available for feedback submission')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const requestBody = {
|
||||
chatId: currentChat.id,
|
||||
userQuery,
|
||||
agentResponse,
|
||||
isPositiveFeedback: isPositive,
|
||||
}
|
||||
|
||||
const response = await fetch('/api/copilot/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to submit feedback: ${response.statusText}`)
|
||||
}
|
||||
|
||||
await response.json()
|
||||
} catch (error) {
|
||||
logger.error('Error submitting feedback:', error)
|
||||
}
|
||||
},
|
||||
[currentChat, getLastUserQuery, getFullAssistantContent, message]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles upvote action
|
||||
*/
|
||||
const handleUpvote = useCallback(async () => {
|
||||
setShowDownvoteSuccess(false)
|
||||
setShowUpvoteSuccess(true)
|
||||
await submitFeedback(true)
|
||||
}, [submitFeedback])
|
||||
|
||||
/**
|
||||
* Handles downvote action
|
||||
*/
|
||||
const handleDownvote = useCallback(async () => {
|
||||
setShowUpvoteSuccess(false)
|
||||
setShowDownvoteSuccess(true)
|
||||
await submitFeedback(false)
|
||||
}, [submitFeedback])
|
||||
|
||||
return {
|
||||
handleUpvote,
|
||||
handleDownvote,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Duration to show success indicators (in milliseconds)
|
||||
*/
|
||||
const SUCCESS_DISPLAY_DURATION = 2000
|
||||
|
||||
/**
|
||||
* Custom hook to manage auto-hiding success states
|
||||
* Automatically hides success indicators after a set duration
|
||||
*
|
||||
* @returns Success state management utilities
|
||||
*/
|
||||
export function useSuccessTimers() {
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
||||
const [showUpvoteSuccess, setShowUpvoteSuccess] = useState(false)
|
||||
const [showDownvoteSuccess, setShowDownvoteSuccess] = useState(false)
|
||||
|
||||
/**
|
||||
* Auto-hide copy success indicator after duration
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (showCopySuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowCopySuccess(false)
|
||||
}, SUCCESS_DISPLAY_DURATION)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [showCopySuccess])
|
||||
|
||||
/**
|
||||
* Auto-hide upvote success indicator after duration
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (showUpvoteSuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowUpvoteSuccess(false)
|
||||
}, SUCCESS_DISPLAY_DURATION)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [showUpvoteSuccess])
|
||||
|
||||
/**
|
||||
* Auto-hide downvote success indicator after duration
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (showDownvoteSuccess) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowDownvoteSuccess(false)
|
||||
}, SUCCESS_DISPLAY_DURATION)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [showDownvoteSuccess])
|
||||
|
||||
/**
|
||||
* Handles copy to clipboard action
|
||||
* @param content - Content to copy to clipboard
|
||||
*/
|
||||
const handleCopy = useCallback((content: string) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
setShowCopySuccess(true)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
// State
|
||||
showCopySuccess,
|
||||
showUpvoteSuccess,
|
||||
showDownvoteSuccess,
|
||||
|
||||
// Operations
|
||||
handleCopy,
|
||||
setShowUpvoteSuccess,
|
||||
setShowDownvoteSuccess,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronUp, LayoutList } from 'lucide-react'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
@@ -530,7 +530,7 @@ function splitActionVerb(text: string): [string | null, string] {
|
||||
* For special tool calls, uses a gradient color. For normal tools, highlights action verbs
|
||||
* in a lighter color with the rest in default gray.
|
||||
*/
|
||||
function ShimmerOverlayText({
|
||||
const ShimmerOverlayText = memo(function ShimmerOverlayText({
|
||||
text,
|
||||
active = false,
|
||||
className,
|
||||
@@ -622,256 +622,7 @@ function ShimmerOverlayText({
|
||||
`}</style>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SubAgentToolCall renders a nested tool call from a subagent in a muted/thinking style.
|
||||
*/
|
||||
function SubAgentToolCall({ toolCall: toolCallProp }: { toolCall: CopilotToolCall }) {
|
||||
// Get live toolCall from store to ensure we have the latest state and params
|
||||
const liveToolCall = useCopilotStore((s) =>
|
||||
toolCallProp.id ? s.toolCallsById[toolCallProp.id] : undefined
|
||||
)
|
||||
const toolCall = liveToolCall || toolCallProp
|
||||
|
||||
const displayName = getDisplayNameForSubAgent(toolCall)
|
||||
|
||||
const isLoading =
|
||||
toolCall.state === ClientToolCallState.generating ||
|
||||
toolCall.state === ClientToolCallState.pending ||
|
||||
toolCall.state === ClientToolCallState.executing
|
||||
|
||||
const showButtons = shouldShowRunSkipButtons(toolCall)
|
||||
const isSpecial = isSpecialToolCall(toolCall)
|
||||
|
||||
// Get params for table rendering
|
||||
const params =
|
||||
(toolCall as any).parameters || (toolCall as any).input || (toolCall as any).params || {}
|
||||
|
||||
// Render table for tools that support it
|
||||
const renderSubAgentTable = () => {
|
||||
if (toolCall.name === 'set_environment_variables') {
|
||||
const variables = params.variables || params.env_vars || {}
|
||||
const entries = Array.isArray(variables)
|
||||
? variables.map((v: any, i: number) => [v.name || `var_${i}`, v.value || ''])
|
||||
: Object.entries(variables).map(([key, val]) => {
|
||||
if (typeof val === 'object' && val !== null && 'value' in (val as any)) {
|
||||
return [key, (val as any).value]
|
||||
}
|
||||
return [key, val]
|
||||
})
|
||||
if (entries.length === 0) return null
|
||||
return (
|
||||
<div className='mt-1.5 w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
|
||||
<table className='w-full table-fixed bg-transparent'>
|
||||
<thead className='bg-transparent'>
|
||||
<tr className='border-[var(--border-1)] border-b bg-transparent'>
|
||||
<th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Variable
|
||||
</th>
|
||||
<th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='bg-transparent'>
|
||||
{entries.map((entry) => {
|
||||
const [key, value] = entry as [string, any]
|
||||
return (
|
||||
<tr key={key} className='border-[var(--border-1)] border-t bg-transparent'>
|
||||
<td className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[6px]'>
|
||||
<span className='truncate font-medium text-[var(--text-primary)] text-xs'>
|
||||
{key}
|
||||
</span>
|
||||
</td>
|
||||
<td className='w-[64%] bg-transparent px-[10px] py-[6px]'>
|
||||
<span className='font-mono text-[var(--text-muted)] text-xs'>
|
||||
{String(value)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolCall.name === 'set_global_workflow_variables') {
|
||||
const ops = Array.isArray(params.operations) ? (params.operations as any[]) : []
|
||||
if (ops.length === 0) return null
|
||||
return (
|
||||
<div className='mt-1.5 w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
|
||||
<div className='grid grid-cols-3 gap-0 border-[var(--border-1)] border-b bg-[var(--surface-4)] py-1.5'>
|
||||
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
|
||||
Name
|
||||
</div>
|
||||
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
|
||||
Type
|
||||
</div>
|
||||
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
<div className='divide-y divide-[var(--border-1)]'>
|
||||
{ops.map((op, idx) => (
|
||||
<div key={idx} className='grid grid-cols-3 gap-0 py-1.5'>
|
||||
<div className='min-w-0 self-start px-2'>
|
||||
<span className='font-season text-[var(--text-primary)] text-xs'>
|
||||
{String(op.name || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div className='self-start px-2'>
|
||||
<span className='rounded border border-[var(--border-1)] px-1 py-0.5 font-[470] font-season text-[10px] text-[var(--text-primary)]'>
|
||||
{String(op.type || '')}
|
||||
</span>
|
||||
</div>
|
||||
<div className='min-w-0 self-start px-2'>
|
||||
<span className='font-[470] font-mono text-[var(--text-muted)] text-xs'>
|
||||
{op.value !== undefined ? String(op.value) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolCall.name === 'run_workflow') {
|
||||
let inputs = params.input || params.inputs || params.workflow_input
|
||||
if (typeof inputs === 'string') {
|
||||
try {
|
||||
inputs = JSON.parse(inputs)
|
||||
} catch {
|
||||
inputs = {}
|
||||
}
|
||||
}
|
||||
if (params.workflow_input && typeof params.workflow_input === 'object') {
|
||||
inputs = params.workflow_input
|
||||
}
|
||||
if (!inputs || typeof inputs !== 'object') {
|
||||
const { workflowId, workflow_input, ...rest } = params
|
||||
inputs = rest
|
||||
}
|
||||
const safeInputs = inputs && typeof inputs === 'object' ? inputs : {}
|
||||
const inputEntries = Object.entries(safeInputs)
|
||||
if (inputEntries.length === 0) return null
|
||||
|
||||
/**
|
||||
* Format a value for display - handles objects, arrays, and primitives
|
||||
*/
|
||||
const formatValue = (value: unknown): string => {
|
||||
if (value === null || value === undefined) return '-'
|
||||
if (typeof value === 'string') return value || '-'
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a complex type (object or array)
|
||||
*/
|
||||
const isComplex = (value: unknown): boolean => {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-1.5 w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'>
|
||||
{/* Header */}
|
||||
<div className='flex items-center gap-[8px] border-[var(--border-1)] border-b bg-[var(--surface-2)] p-[8px]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-primary)]'>Input</span>
|
||||
<span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'>
|
||||
{inputEntries.length}
|
||||
</span>
|
||||
</div>
|
||||
{/* Input entries */}
|
||||
<div className='flex flex-col'>
|
||||
{inputEntries.map(([key, value], index) => {
|
||||
const formattedValue = formatValue(value)
|
||||
const needsCodeViewer = isComplex(value)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={clsx(
|
||||
'flex flex-col gap-1 px-[10px] py-[6px]',
|
||||
index > 0 && 'border-[var(--border-1)] border-t'
|
||||
)}
|
||||
>
|
||||
{/* Input key */}
|
||||
<span className='font-medium text-[11px] text-[var(--text-primary)]'>{key}</span>
|
||||
{/* Value display */}
|
||||
{needsCodeViewer ? (
|
||||
<Code.Viewer
|
||||
code={formattedValue}
|
||||
language='json'
|
||||
showGutter={false}
|
||||
className='max-h-[80px] min-h-0'
|
||||
/>
|
||||
) : (
|
||||
<span className='font-mono text-[11px] text-[var(--text-muted)] leading-[1.3]'>
|
||||
{formattedValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// For edit_workflow, only show the WorkflowEditSummary component (replaces text display)
|
||||
const isEditWorkflow = toolCall.name === 'edit_workflow'
|
||||
const hasOperations = Array.isArray(params.operations) && params.operations.length > 0
|
||||
|
||||
return (
|
||||
<div className='py-0.5'>
|
||||
{/* Hide text display for edit_workflow when we have operations to show in summary */}
|
||||
{!(isEditWorkflow && hasOperations) && (
|
||||
<ShimmerOverlayText
|
||||
text={displayName}
|
||||
active={isLoading && !showButtons}
|
||||
isSpecial={isSpecial}
|
||||
className='font-[470] font-season text-[12px] text-[var(--text-tertiary)]'
|
||||
/>
|
||||
)}
|
||||
{renderSubAgentTable()}
|
||||
{/* WorkflowEditSummary is rendered outside SubAgentContent for edit subagent */}
|
||||
{showButtons && <RunSkipButtons toolCall={toolCall} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for subagent tool calls
|
||||
*/
|
||||
function getDisplayNameForSubAgent(toolCall: CopilotToolCall): string {
|
||||
const fromStore = toolCall.display?.text
|
||||
if (fromStore) return fromStore
|
||||
|
||||
const stateVerb = getStateVerb(toolCall.state)
|
||||
const formattedName = formatToolName(toolCall.name)
|
||||
return `${stateVerb} ${formattedName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Max height for subagent content before internal scrolling kicks in
|
||||
*/
|
||||
const SUBAGENT_MAX_HEIGHT = 200
|
||||
|
||||
/**
|
||||
* Interval for auto-scroll during streaming (ms)
|
||||
*/
|
||||
const SUBAGENT_SCROLL_INTERVAL = 100
|
||||
})
|
||||
|
||||
/**
|
||||
* Get the outer collapse header label for completed subagent tools.
|
||||
@@ -882,236 +633,6 @@ function getSubagentCompletionLabel(toolName: string): string {
|
||||
return labels?.completed ?? 'Thought'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display labels for subagent tools.
|
||||
* Uses the tool's UI config.
|
||||
*/
|
||||
function getSubagentLabels(toolName: string, isStreaming: boolean): string {
|
||||
const labels = getSubagentLabelsFromConfig(toolName, isStreaming)
|
||||
if (labels) {
|
||||
return isStreaming ? labels.streaming : labels.completed
|
||||
}
|
||||
return isStreaming ? 'Processing' : 'Processed'
|
||||
}
|
||||
|
||||
/**
|
||||
* SubAgentContent renders the streamed content and tool calls from a subagent
|
||||
* with thinking-style styling (same as ThinkingBlock).
|
||||
* Auto-collapses when streaming ends and has internal scrolling for long content.
|
||||
*/
|
||||
function SubAgentContent({
|
||||
blocks,
|
||||
isStreaming = false,
|
||||
toolName = 'debug',
|
||||
}: {
|
||||
blocks?: SubAgentContentBlock[]
|
||||
isStreaming?: boolean
|
||||
toolName?: string
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [userHasScrolledAway, setUserHasScrolledAway] = useState(false)
|
||||
const userCollapsedRef = useRef<boolean>(false)
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
const lastScrollTopRef = useRef(0)
|
||||
const programmaticScrollRef = useRef(false)
|
||||
|
||||
// Check if there are any tool calls (which means thinking should close)
|
||||
const hasToolCalls = useMemo(() => {
|
||||
if (!blocks) return false
|
||||
return blocks.some((b) => b.type === 'subagent_tool_call' && b.toolCall)
|
||||
}, [blocks])
|
||||
|
||||
// Auto-expand when streaming with content, auto-collapse when done or when tool call comes in
|
||||
useEffect(() => {
|
||||
if (!isStreaming || hasToolCalls) {
|
||||
setIsExpanded(false)
|
||||
userCollapsedRef.current = false
|
||||
setUserHasScrolledAway(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!userCollapsedRef.current && blocks && blocks.length > 0) {
|
||||
setIsExpanded(true)
|
||||
}
|
||||
}, [isStreaming, blocks, hasToolCalls])
|
||||
|
||||
// Handle scroll events to detect user scrolling away
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container || !isExpanded) return
|
||||
|
||||
const handleScroll = () => {
|
||||
if (programmaticScrollRef.current) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = container
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
const isNearBottom = distanceFromBottom <= 20
|
||||
|
||||
const delta = scrollTop - lastScrollTopRef.current
|
||||
const movedUp = delta < -2
|
||||
|
||||
if (movedUp && !isNearBottom) {
|
||||
setUserHasScrolledAway(true)
|
||||
}
|
||||
|
||||
// Re-stick if user scrolls back to bottom
|
||||
if (userHasScrolledAway && isNearBottom) {
|
||||
setUserHasScrolledAway(false)
|
||||
}
|
||||
|
||||
lastScrollTopRef.current = scrollTop
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', handleScroll, { passive: true })
|
||||
lastScrollTopRef.current = container.scrollTop
|
||||
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
}, [isExpanded, userHasScrolledAway])
|
||||
|
||||
// Smart auto-scroll: only scroll if user hasn't scrolled away
|
||||
useEffect(() => {
|
||||
if (!isStreaming || !isExpanded || userHasScrolledAway) return
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
const container = scrollContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = container
|
||||
const distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
||||
const isNearBottom = distanceFromBottom <= 50
|
||||
|
||||
if (isNearBottom) {
|
||||
programmaticScrollRef.current = true
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
window.setTimeout(() => {
|
||||
programmaticScrollRef.current = false
|
||||
}, 150)
|
||||
}
|
||||
}, SUBAGENT_SCROLL_INTERVAL)
|
||||
|
||||
return () => window.clearInterval(intervalId)
|
||||
}, [isStreaming, isExpanded, userHasScrolledAway])
|
||||
|
||||
if (!blocks || blocks.length === 0) return null
|
||||
|
||||
const hasContent = blocks.length > 0
|
||||
// Show "done" label when streaming ends OR when tool calls are present
|
||||
const isThinkingDone = !isStreaming || hasToolCalls
|
||||
const label = getSubagentLabels(toolName, !isThinkingDone)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Define shimmer keyframes */}
|
||||
{!isThinkingDone && (
|
||||
<style>{`
|
||||
@keyframes thinking-shimmer {
|
||||
0% { background-position: 150% 0; }
|
||||
50% { background-position: 0% 0; }
|
||||
100% { background-position: -150% 0; }
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsExpanded((v) => {
|
||||
const next = !v
|
||||
if (!next && isStreaming) userCollapsedRef.current = true
|
||||
return next
|
||||
})
|
||||
}}
|
||||
className='group inline-flex items-center gap-1 text-left font-[470] font-season text-[var(--text-secondary)] text-sm transition-colors hover:text-[var(--text-primary)]'
|
||||
type='button'
|
||||
disabled={!hasContent}
|
||||
>
|
||||
<span className='relative inline-block'>
|
||||
<span className='text-[var(--text-tertiary)]'>{label}</span>
|
||||
{!isThinkingDone && (
|
||||
<span
|
||||
aria-hidden='true'
|
||||
className='pointer-events-none absolute inset-0 select-none overflow-hidden'
|
||||
>
|
||||
<span
|
||||
className='block text-transparent'
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 50%, rgba(255,255,255,0) 100%)',
|
||||
backgroundSize: '200% 100%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
WebkitBackgroundClip: 'text',
|
||||
backgroundClip: 'text',
|
||||
animation: 'thinking-shimmer 1.4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{hasContent && (
|
||||
<ChevronUp
|
||||
className={clsx(
|
||||
'h-3 w-3 transition-all group-hover:opacity-100',
|
||||
isExpanded ? 'rotate-180 opacity-100' : 'rotate-90 opacity-0'
|
||||
)}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={clsx(
|
||||
'overflow-y-auto transition-all duration-150 ease-out',
|
||||
isExpanded ? 'mt-1.5 max-h-[200px] opacity-100' : 'max-h-0 opacity-0'
|
||||
)}
|
||||
>
|
||||
{blocks.map((block, index) => {
|
||||
if (block.type === 'subagent_text' && block.content) {
|
||||
const isLastBlock = index === blocks.length - 1
|
||||
// Strip special tags from display (they're rendered separately)
|
||||
const parsed = parseSpecialTags(block.content)
|
||||
const displayContent = parsed.cleanContent
|
||||
if (!displayContent) return null
|
||||
return (
|
||||
<pre
|
||||
key={`subagent-text-${index}`}
|
||||
className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'
|
||||
>
|
||||
{displayContent}
|
||||
{!isThinkingDone && isLastBlock && (
|
||||
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-tertiary)]' />
|
||||
)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
// All tool calls are rendered at top level, skip here
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Render PlanSteps for plan subagent when content contains <plan> tag */}
|
||||
{toolName === 'plan' &&
|
||||
(() => {
|
||||
// Combine all text content from blocks
|
||||
const allText = blocks
|
||||
.filter((b) => b.type === 'subagent_text' && b.content)
|
||||
.map((b) => b.content)
|
||||
.join('')
|
||||
const parsed = parseSpecialTags(allText)
|
||||
if (parsed.plan && Object.keys(parsed.plan).length > 0) {
|
||||
return <PlanSteps steps={parsed.plan} streaming={!isThinkingDone} />
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SubAgentThinkingContent renders subagent blocks as simple thinking text (ThinkingBlock).
|
||||
* Used for inline rendering within regular tool calls that have subagent content.
|
||||
@@ -1123,7 +644,6 @@ function SubAgentThinkingContent({
|
||||
blocks: SubAgentContentBlock[]
|
||||
isStreaming?: boolean
|
||||
}) {
|
||||
// Combine all text content from blocks
|
||||
let allRawText = ''
|
||||
let cleanText = ''
|
||||
for (const block of blocks) {
|
||||
@@ -1134,12 +654,10 @@ function SubAgentThinkingContent({
|
||||
}
|
||||
}
|
||||
|
||||
// Parse plan from all text
|
||||
const allParsed = parseSpecialTags(allRawText)
|
||||
|
||||
if (!cleanText.trim() && !allParsed.plan) return null
|
||||
|
||||
// Check if special tags are present
|
||||
const hasSpecialTags = !!(allParsed.plan && Object.keys(allParsed.plan).length > 0)
|
||||
|
||||
return (
|
||||
@@ -1172,7 +690,7 @@ const COLLAPSIBLE_SUBAGENTS = new Set(['plan', 'debug', 'research'])
|
||||
* - When done (not streaming): Most subagents stay expanded, only specific ones collapse
|
||||
* - Exception: plan, debug, research, info subagents collapse into a header
|
||||
*/
|
||||
function SubagentContentRenderer({
|
||||
const SubagentContentRenderer = memo(function SubagentContentRenderer({
|
||||
toolCall,
|
||||
shouldCollapse,
|
||||
}: {
|
||||
@@ -1182,36 +700,26 @@ function SubagentContentRenderer({
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const startTimeRef = useRef<number>(Date.now())
|
||||
const wasStreamingRef = useRef(false)
|
||||
|
||||
const isStreaming = !!toolCall.subAgentStreaming
|
||||
|
||||
// Reset start time when streaming begins
|
||||
useEffect(() => {
|
||||
if (isStreaming) {
|
||||
if (isStreaming && !wasStreamingRef.current) {
|
||||
startTimeRef.current = Date.now()
|
||||
setDuration(0)
|
||||
wasStreamingRef.current = true
|
||||
} else if (!isStreaming && wasStreamingRef.current) {
|
||||
setDuration(Date.now() - startTimeRef.current)
|
||||
wasStreamingRef.current = false
|
||||
}
|
||||
}, [isStreaming])
|
||||
|
||||
// Update duration timer during streaming
|
||||
useEffect(() => {
|
||||
if (!isStreaming) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setDuration(Date.now() - startTimeRef.current)
|
||||
}, 100)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isStreaming])
|
||||
|
||||
// Auto-collapse when streaming ends (only for collapsible subagents)
|
||||
useEffect(() => {
|
||||
if (!isStreaming && shouldCollapse) {
|
||||
setIsExpanded(false)
|
||||
}
|
||||
}, [isStreaming, shouldCollapse])
|
||||
|
||||
// Build segments: each segment is either text content or a tool call
|
||||
const segments: Array<
|
||||
{ type: 'text'; content: string } | { type: 'tool'; block: SubAgentContentBlock }
|
||||
> = []
|
||||
@@ -1235,7 +743,6 @@ function SubagentContentRenderer({
|
||||
segments.push({ type: 'text', content: currentText })
|
||||
}
|
||||
|
||||
// Parse plan and options
|
||||
const allParsed = parseSpecialTags(allRawText)
|
||||
const hasSpecialTags = !!(
|
||||
(allParsed.plan && Object.keys(allParsed.plan).length > 0) ||
|
||||
@@ -1247,15 +754,11 @@ function SubagentContentRenderer({
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
// Outer header uses subagent-specific label
|
||||
const outerLabel = getSubagentCompletionLabel(toolCall.name)
|
||||
const durationText = `${outerLabel} for ${formatDuration(duration)}`
|
||||
|
||||
// Check if we have a plan to render outside the collapsible
|
||||
const hasPlan = allParsed.plan && Object.keys(allParsed.plan).length > 0
|
||||
|
||||
// Render the collapsible content (thinking blocks + tool calls, NOT plan)
|
||||
// Inner thinking text always uses "Thought" label
|
||||
const renderCollapsibleContent = () => (
|
||||
<>
|
||||
{segments.map((segment, index) => {
|
||||
@@ -1275,7 +778,6 @@ function SubagentContentRenderer({
|
||||
)
|
||||
}
|
||||
if (segment.type === 'tool' && segment.block.toolCall) {
|
||||
// For edit subagent's edit_workflow tool: only show the diff summary, skip the tool call header
|
||||
if (toolCall.name === 'edit' && segment.block.toolCall.name === 'edit_workflow') {
|
||||
return (
|
||||
<div key={`tool-${segment.block.toolCall.id || index}`}>
|
||||
@@ -1294,7 +796,6 @@ function SubagentContentRenderer({
|
||||
</>
|
||||
)
|
||||
|
||||
// During streaming OR for non-collapsible subagents: show content at top level
|
||||
if (isStreaming || !shouldCollapse) {
|
||||
return (
|
||||
<div className='w-full space-y-1.5'>
|
||||
@@ -1304,8 +805,6 @@ function SubagentContentRenderer({
|
||||
)
|
||||
}
|
||||
|
||||
// Completed collapsible subagent (plan, debug, research, info): show collapsible header
|
||||
// Plan artifact stays outside the collapsible
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<button
|
||||
@@ -1336,7 +835,7 @@ function SubagentContentRenderer({
|
||||
{hasPlan && <PlanSteps steps={allParsed.plan!} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Determines if a tool call is "special" and should display with gradient styling.
|
||||
@@ -1351,15 +850,15 @@ function isSpecialToolCall(toolCall: CopilotToolCall): boolean {
|
||||
* Displays: workflow name with stats (+N green, N orange, -N red)
|
||||
* Expands inline on click to show individual blocks with their icons.
|
||||
*/
|
||||
function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
// Get block data from current workflow state
|
||||
const WorkflowEditSummary = memo(function WorkflowEditSummary({
|
||||
toolCall,
|
||||
}: {
|
||||
toolCall: CopilotToolCall
|
||||
}) {
|
||||
const blocks = useWorkflowStore((s) => s.blocks)
|
||||
|
||||
// Cache block info on first render (before diff is applied) so we can show
|
||||
// deleted blocks properly even after they're removed from the workflow
|
||||
const cachedBlockInfoRef = useRef<Record<string, { name: string; type: string }>>({})
|
||||
|
||||
// Update cache with current block info (only add, never remove)
|
||||
useEffect(() => {
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
if (!cachedBlockInfoRef.current[blockId]) {
|
||||
@@ -1371,22 +870,18 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
}
|
||||
}, [blocks])
|
||||
|
||||
// Show for edit_workflow regardless of state
|
||||
if (toolCall.name !== 'edit_workflow') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Extract operations from tool call params
|
||||
const params =
|
||||
(toolCall as any).parameters || (toolCall as any).input || (toolCall as any).params || {}
|
||||
let operations = Array.isArray(params.operations) ? params.operations : []
|
||||
|
||||
// Fallback: check if operations are at top level of toolCall
|
||||
if (operations.length === 0 && Array.isArray((toolCall as any).operations)) {
|
||||
operations = (toolCall as any).operations
|
||||
}
|
||||
|
||||
// Group operations by type with block info
|
||||
interface SubBlockPreview {
|
||||
id: string
|
||||
title: string
|
||||
@@ -1412,25 +907,20 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
const blockId = op.block_id
|
||||
if (!blockId) continue
|
||||
|
||||
// Get block info from current workflow state, cached state, or operation params
|
||||
const currentBlock = blocks[blockId]
|
||||
const cachedBlock = cachedBlockInfoRef.current[blockId]
|
||||
let blockName = currentBlock?.name || cachedBlock?.name || ''
|
||||
let blockType = currentBlock?.type || cachedBlock?.type || ''
|
||||
|
||||
// For add operations, get info from params (type is stored as params.type)
|
||||
if (op.operation_type === 'add' && op.params) {
|
||||
blockName = blockName || op.params.name || ''
|
||||
blockType = blockType || op.params.type || ''
|
||||
}
|
||||
|
||||
// For edit operations, also check params.type if block not in current state
|
||||
if (op.operation_type === 'edit' && op.params && !blockType) {
|
||||
blockType = op.params.type || ''
|
||||
}
|
||||
|
||||
// Skip edge-only edit operations (like how we don't highlight blocks on canvas for edge changes)
|
||||
// An edit is edge-only if params only contains 'connections' and nothing else meaningful
|
||||
if (op.operation_type === 'edit' && op.params) {
|
||||
const paramKeys = Object.keys(op.params)
|
||||
const isEdgeOnlyEdit = paramKeys.length === 1 && paramKeys[0] === 'connections'
|
||||
@@ -1439,9 +929,7 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
}
|
||||
}
|
||||
|
||||
// For delete operations, check if block info was provided in operation
|
||||
if (op.operation_type === 'delete') {
|
||||
// Some delete operations may include block_name and block_type
|
||||
blockName = blockName || op.block_name || ''
|
||||
blockType = blockType || op.block_type || ''
|
||||
}
|
||||
@@ -1453,16 +941,12 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
|
||||
const change: BlockChange = { blockId, blockName, blockType }
|
||||
|
||||
// Extract subblock info from operation params, ordered by block config
|
||||
if (op.params?.inputs && typeof op.params.inputs === 'object') {
|
||||
const inputs = op.params.inputs as Record<string, unknown>
|
||||
const blockConfig = getBlock(blockType)
|
||||
|
||||
// Build subBlocks array
|
||||
const subBlocks: SubBlockPreview[] = []
|
||||
|
||||
// Special handling for condition blocks - parse conditions JSON and render as separate rows
|
||||
// This matches how the canvas renders condition blocks with "if", "else if", "else" rows
|
||||
if (blockType === 'condition' && 'conditions' in inputs) {
|
||||
const conditionsValue = inputs.conditions
|
||||
const raw = typeof conditionsValue === 'string' ? conditionsValue : undefined
|
||||
@@ -1484,35 +968,26 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback: show default if/else
|
||||
subBlocks.push({ id: 'if', title: 'if', value: '', isPassword: false })
|
||||
subBlocks.push({ id: 'else', title: 'else', value: '', isPassword: false })
|
||||
}
|
||||
} else {
|
||||
// Filter visible subblocks from config (same logic as canvas preview)
|
||||
const visibleSubBlocks =
|
||||
blockConfig?.subBlocks?.filter((sb) => {
|
||||
// Skip hidden subblocks
|
||||
if (sb.hidden) return false
|
||||
if (sb.hideFromPreview) return false
|
||||
// Skip advanced mode subblocks (not visible by default)
|
||||
if (sb.mode === 'advanced') return false
|
||||
// Skip trigger mode subblocks
|
||||
if (sb.mode === 'trigger') return false
|
||||
return true
|
||||
}) ?? []
|
||||
|
||||
// Track seen ids to dedupe (same pattern as canvas preview using id as key)
|
||||
const seenIds = new Set<string>()
|
||||
|
||||
// Add subblocks that are visible in config, in config order (first config per id wins)
|
||||
for (const subBlockConfig of visibleSubBlocks) {
|
||||
// Skip if we've already added this id (handles configs with same id but different conditions)
|
||||
if (seenIds.has(subBlockConfig.id)) continue
|
||||
|
||||
if (subBlockConfig.id in inputs) {
|
||||
const value = inputs[subBlockConfig.id]
|
||||
// Skip empty values and connections
|
||||
if (value === null || value === undefined || value === '') continue
|
||||
seenIds.add(subBlockConfig.id)
|
||||
subBlocks.push({
|
||||
@@ -1553,12 +1028,10 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get block config by type (for icon and bgColor)
|
||||
const getBlockConfig = (blockType: string) => {
|
||||
return getBlock(blockType)
|
||||
}
|
||||
|
||||
// Render a single block item with action icon and details
|
||||
const renderBlockItem = (change: BlockChange, type: 'add' | 'edit' | 'delete') => {
|
||||
const blockConfig = getBlockConfig(change.blockType)
|
||||
const Icon = blockConfig?.icon
|
||||
@@ -1631,23 +1104,20 @@ function WorkflowEditSummary({ toolCall }: { toolCall: CopilotToolCall }) {
|
||||
{deletedBlocks.map((change) => renderBlockItem(change, 'delete'))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a tool is an integration tool (server-side executed, not a client tool)
|
||||
*/
|
||||
function isIntegrationTool(toolName: string): boolean {
|
||||
// Any tool NOT in CLASS_TOOL_METADATA is an integration tool (server-side execution)
|
||||
return !CLASS_TOOL_METADATA[toolName]
|
||||
}
|
||||
|
||||
function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||
// First check UI config for interrupt
|
||||
if (hasInterruptFromConfig(toolCall.name) && toolCall.state === 'pending') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Then check instance-level interrupt
|
||||
const instance = getClientTool(toolCall.id)
|
||||
let hasInterrupt = !!instance?.getInterruptDisplays?.()
|
||||
if (!hasInterrupt) {
|
||||
@@ -1662,12 +1132,10 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Show buttons for client tools with interrupts
|
||||
if (hasInterrupt && toolCall.state === 'pending') {
|
||||
return true
|
||||
}
|
||||
|
||||
// Always show buttons for integration tools in pending state (they need user confirmation)
|
||||
const mode = useCopilotStore.getState().mode
|
||||
if (mode === 'build' && isIntegrationTool(toolCall.name) && toolCall.state === 'pending') {
|
||||
return true
|
||||
@@ -1684,19 +1152,14 @@ async function handleRun(
|
||||
) {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
|
||||
// Handle integration tools (server-side execution)
|
||||
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||
// Set executing state immediately for UI feedback
|
||||
setToolCallState(toolCall, 'executing')
|
||||
onStateChange?.('executing')
|
||||
try {
|
||||
await useCopilotStore.getState().executeIntegrationTool(toolCall.id)
|
||||
// Note: executeIntegrationTool handles success/error state updates internally
|
||||
} catch (e) {
|
||||
// If executeIntegrationTool throws, ensure we update state to error
|
||||
setToolCallState(toolCall, 'error', { error: e instanceof Error ? e.message : String(e) })
|
||||
onStateChange?.('error')
|
||||
// Notify backend about the error so agent doesn't hang
|
||||
try {
|
||||
await fetch('/api/copilot/tools/mark-complete', {
|
||||
method: 'POST',
|
||||
@@ -1710,7 +1173,6 @@ async function handleRun(
|
||||
}),
|
||||
})
|
||||
} catch {
|
||||
// Last resort: log error if we can't notify backend
|
||||
console.error('[handleRun] Failed to notify backend of tool error:', toolCall.id)
|
||||
}
|
||||
}
|
||||
@@ -1735,13 +1197,10 @@ async function handleRun(
|
||||
async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onStateChange?: any) {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
|
||||
// Handle integration tools (skip by marking as rejected and notifying backend)
|
||||
if (!instance && isIntegrationTool(toolCall.name)) {
|
||||
setToolCallState(toolCall, 'rejected')
|
||||
onStateChange?.('rejected')
|
||||
|
||||
// Notify backend that tool was skipped - this is CRITICAL for the agent to continue
|
||||
// Retry up to 3 times if the notification fails
|
||||
let notified = false
|
||||
for (let attempt = 0; attempt < 3 && !notified; attempt++) {
|
||||
try {
|
||||
@@ -1760,7 +1219,6 @@ async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onSt
|
||||
notified = true
|
||||
}
|
||||
} catch (e) {
|
||||
// Wait briefly before retry
|
||||
if (attempt < 2) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
@@ -1783,7 +1241,6 @@ async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onSt
|
||||
}
|
||||
|
||||
function getDisplayName(toolCall: CopilotToolCall): string {
|
||||
// Prefer display resolved in the copilot store (SSOT)
|
||||
const fromStore = (toolCall as any).display?.text
|
||||
if (fromStore) return fromStore
|
||||
try {
|
||||
@@ -1792,8 +1249,6 @@ function getDisplayName(toolCall: CopilotToolCall): string {
|
||||
if (byState?.text) return byState.text
|
||||
} catch {}
|
||||
|
||||
// For integration tools, format the tool name nicely
|
||||
// e.g., "google_calendar_list_events" -> "Running Google Calendar List Events"
|
||||
const stateVerb = getStateVerb(toolCall.state)
|
||||
const formattedName = formatToolName(toolCall.name)
|
||||
return `${stateVerb} ${formattedName}`
|
||||
@@ -2308,11 +1763,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
}
|
||||
|
||||
if (toolCall.name === 'run_workflow') {
|
||||
// Get inputs - could be in multiple locations
|
||||
let inputs = editedParams.input || editedParams.inputs || editedParams.workflow_input
|
||||
let isNestedInWorkflowInput = false
|
||||
|
||||
// If input is a JSON string, parse it
|
||||
if (typeof inputs === 'string') {
|
||||
try {
|
||||
inputs = JSON.parse(inputs)
|
||||
@@ -2321,13 +1774,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
}
|
||||
}
|
||||
|
||||
// Check if workflow_input exists and contains the actual inputs
|
||||
if (editedParams.workflow_input && typeof editedParams.workflow_input === 'object') {
|
||||
inputs = editedParams.workflow_input
|
||||
isNestedInWorkflowInput = true
|
||||
}
|
||||
|
||||
// If no inputs object found, treat base editedParams as inputs (excluding system fields)
|
||||
if (!inputs || typeof inputs !== 'object') {
|
||||
const { workflowId, workflow_input, ...rest } = editedParams
|
||||
inputs = rest
|
||||
@@ -2336,7 +1787,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
const safeInputs = inputs && typeof inputs === 'object' ? inputs : {}
|
||||
const inputEntries = Object.entries(safeInputs)
|
||||
|
||||
// Don't show the section if there are no inputs
|
||||
if (inputEntries.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -2348,7 +1798,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
if (value === null || value === undefined) return ''
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
||||
// For objects and arrays, use JSON.stringify with formatting
|
||||
try {
|
||||
return JSON.stringify(value, null, 2)
|
||||
} catch {
|
||||
@@ -2360,11 +1809,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
* Parse a string value back to its original type if possible
|
||||
*/
|
||||
const parseInputValue = (value: string, originalValue: unknown): unknown => {
|
||||
// If original was a primitive, keep as string
|
||||
if (typeof originalValue !== 'object' || originalValue === null) {
|
||||
return value
|
||||
}
|
||||
// Try to parse as JSON for objects/arrays
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
@@ -2473,7 +1920,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
return null
|
||||
}
|
||||
|
||||
// Special handling for tools with alwaysExpanded config (e.g., set_environment_variables)
|
||||
const isAlwaysExpanded = toolUIConfig?.alwaysExpanded
|
||||
if (
|
||||
(isAlwaysExpanded || toolCall.name === 'set_environment_variables') &&
|
||||
@@ -2531,7 +1977,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
)
|
||||
}
|
||||
|
||||
// Special rendering for tools with 'code' customRenderer (e.g., function_execute)
|
||||
if (toolUIConfig?.customRenderer === 'code' || toolCall.name === 'function_execute') {
|
||||
const code = params.code || ''
|
||||
const isFunctionExecuteClickable = isAutoAllowed
|
||||
@@ -2593,7 +2038,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
)
|
||||
}
|
||||
|
||||
// Determine if tool name should be clickable (expandable tools or auto-allowed integration tools)
|
||||
const isToolNameClickable = isExpandableTool || isAutoAllowed
|
||||
|
||||
const handleToolNameClick = () => {
|
||||
@@ -2604,7 +2048,6 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
}
|
||||
}
|
||||
|
||||
// For edit_workflow, hide text display when we have operations (WorkflowEditSummary replaces it)
|
||||
const isEditWorkflow = toolCall.name === 'edit_workflow'
|
||||
const hasOperations = Array.isArray(params.operations) && params.operations.length > 0
|
||||
const hideTextForEditWorkflow = isEditWorkflow && hasOperations
|
||||
@@ -2649,13 +2092,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
onClick={async () => {
|
||||
try {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
// Transition to background state locally so UI updates immediately
|
||||
instance?.setState?.((ClientToolCallState as any).background)
|
||||
await instance?.markToolComplete?.(
|
||||
200,
|
||||
'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete'
|
||||
)
|
||||
// Optionally force a re-render; store should sync state from server
|
||||
forceUpdate({})
|
||||
onStateChange?.('background')
|
||||
} catch {}
|
||||
@@ -2672,21 +2113,16 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
|
||||
onClick={async () => {
|
||||
try {
|
||||
const instance = getClientTool(toolCall.id)
|
||||
// Get elapsed seconds before waking
|
||||
const elapsedSeconds = instance?.getElapsedSeconds?.() || 0
|
||||
// Transition to background state locally so UI updates immediately
|
||||
// Pass elapsed seconds in the result so dynamic text can use it
|
||||
instance?.setState?.((ClientToolCallState as any).background, {
|
||||
result: { _elapsedSeconds: elapsedSeconds },
|
||||
})
|
||||
// Update the tool call params in the store to include elapsed time for display
|
||||
const { updateToolCallParams } = useCopilotStore.getState()
|
||||
updateToolCallParams?.(toolCall.id, { _elapsedSeconds: Math.round(elapsedSeconds) })
|
||||
await instance?.markToolComplete?.(
|
||||
200,
|
||||
`User woke you up after ${Math.round(elapsedSeconds)} seconds`
|
||||
)
|
||||
// Optionally force a re-render; store should sync state from server
|
||||
forceUpdate({})
|
||||
onStateChange?.('background')
|
||||
} catch {}
|
||||
|
||||
@@ -151,7 +151,7 @@ export function useMentionData(props: UseMentionDataProps): MentionDataReturn {
|
||||
useShallow(useCallback((state) => Object.keys(state.blocks), []))
|
||||
)
|
||||
|
||||
const registryWorkflows = useWorkflowRegistry((state) => state.workflows)
|
||||
const registryWorkflows = useWorkflowRegistry(useShallow((state) => state.workflows))
|
||||
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
|
||||
const isLoadingWorkflows =
|
||||
hydrationPhase === 'idle' ||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { useMentionMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-menu'
|
||||
import type { ChatContext } from '@/stores/panel'
|
||||
|
||||
@@ -39,11 +39,11 @@ export function useMentionTokens({
|
||||
setSelectedContexts,
|
||||
}: UseMentionTokensProps) {
|
||||
/**
|
||||
* Computes all mention ranges in the message (both @mentions and /commands)
|
||||
*
|
||||
* @returns Array of mention ranges sorted by start position
|
||||
* Memoized mention ranges - computed once when message or selectedContexts change.
|
||||
* This prevents expensive O(n×m) string searches from running on every keystroke
|
||||
* when other callbacks access the ranges.
|
||||
*/
|
||||
const computeMentionRanges = useCallback((): MentionRange[] => {
|
||||
const memoizedMentionRanges = useMemo((): MentionRange[] => {
|
||||
const ranges: MentionRange[] = []
|
||||
if (!message || selectedContexts.length === 0) return ranges
|
||||
|
||||
@@ -93,35 +93,45 @@ export function useMentionTokens({
|
||||
|
||||
/**
|
||||
* Finds a mention range containing the given position
|
||||
*/
|
||||
const computeMentionRanges = useCallback(
|
||||
(): MentionRange[] => memoizedMentionRanges,
|
||||
[memoizedMentionRanges]
|
||||
)
|
||||
|
||||
/**
|
||||
* Finds a mention range containing the given position.
|
||||
* Uses memoized ranges directly for better performance.
|
||||
*
|
||||
* @param pos - Position to check
|
||||
* @returns Mention range if found, undefined otherwise
|
||||
*/
|
||||
const findRangeContaining = useCallback(
|
||||
(pos: number): MentionRange | undefined => {
|
||||
const ranges = computeMentionRanges()
|
||||
return ranges.find((r) => pos > r.start && pos < r.end)
|
||||
return memoizedMentionRanges.find((r) => pos > r.start && pos < r.end)
|
||||
},
|
||||
[computeMentionRanges]
|
||||
[memoizedMentionRanges]
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes contexts for mention tokens that overlap with a text selection
|
||||
* Removes contexts for mention tokens that overlap with a text selection.
|
||||
* Uses memoized ranges directly for better performance.
|
||||
*
|
||||
* @param selStart - Selection start position
|
||||
* @param selEnd - Selection end position
|
||||
*/
|
||||
const removeContextsInSelection = useCallback(
|
||||
(selStart: number, selEnd: number) => {
|
||||
const ranges = computeMentionRanges()
|
||||
const overlappingRanges = ranges.filter((r) => !(selEnd <= r.start || selStart >= r.end))
|
||||
const overlappingRanges = memoizedMentionRanges.filter(
|
||||
(r) => !(selEnd <= r.start || selStart >= r.end)
|
||||
)
|
||||
|
||||
if (overlappingRanges.length > 0) {
|
||||
const labelsToRemove = new Set(overlappingRanges.map((r) => r.label))
|
||||
setSelectedContexts((prev) => prev.filter((c) => !c.label || !labelsToRemove.has(c.label)))
|
||||
}
|
||||
},
|
||||
[computeMentionRanges, setSelectedContexts]
|
||||
[memoizedMentionRanges, setSelectedContexts]
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -655,6 +655,13 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
[insertTriggerAndOpenMenu]
|
||||
)
|
||||
|
||||
const handleModelSelect = useCallback(
|
||||
(model: string) => {
|
||||
setSelectedModel(model as any)
|
||||
},
|
||||
[setSelectedModel]
|
||||
)
|
||||
|
||||
const canSubmit = message.trim().length > 0 && !disabled && !isLoading
|
||||
const showAbortButton = isLoading && onAbort
|
||||
|
||||
@@ -863,7 +870,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
|
||||
<ModelSelector
|
||||
selectedModel={selectedModel}
|
||||
isNearTop={isNearTop}
|
||||
onModelSelect={(model: string) => setSelectedModel(model as any)}
|
||||
onModelSelect={handleModelSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
ButtonGroupItem,
|
||||
Checkbox,
|
||||
Code,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
Input,
|
||||
Label,
|
||||
TagInput,
|
||||
@@ -271,14 +269,6 @@ export function A2aDeploy({
|
||||
onNeedsRepublishChange?.(!!needsRepublish)
|
||||
}, [needsRepublish, onNeedsRepublishChange])
|
||||
|
||||
const authSchemeOptions: ComboboxOption[] = useMemo(
|
||||
() => [
|
||||
{ label: 'API Key', value: 'apiKey' },
|
||||
{ label: 'None (Public)', value: 'none' },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const canSave = name.trim().length > 0 && description.trim().length > 0
|
||||
useEffect(() => {
|
||||
onCanSaveChange?.(canSave)
|
||||
@@ -758,17 +748,18 @@ console.log(data);`
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Authentication */}
|
||||
{/* Access */}
|
||||
<div>
|
||||
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Authentication
|
||||
Access
|
||||
</Label>
|
||||
<Combobox
|
||||
options={authSchemeOptions}
|
||||
<ButtonGroup
|
||||
value={authScheme}
|
||||
onChange={(v) => setAuthScheme(v as AuthScheme)}
|
||||
placeholder='Select authentication...'
|
||||
/>
|
||||
onValueChange={(value) => setAuthScheme(value as AuthScheme)}
|
||||
>
|
||||
<ButtonGroupItem value='apiKey'>API Key</ButtonGroupItem>
|
||||
<ButtonGroupItem value='none'>Public</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
<p className='mt-[6.5px] text-[11px] text-[var(--text-secondary)]'>
|
||||
{authScheme === 'none'
|
||||
? 'Anyone can call this agent without authentication'
|
||||
|
||||
@@ -424,7 +424,7 @@ export function ChatDeploy({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='default' onClick={handleDelete} disabled={isDeleting}>
|
||||
<Button variant='destructive' onClick={handleDelete} disabled={isDeleting}>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { Check, Copy, Wand2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import 'prismjs/components/prism-python'
|
||||
@@ -170,7 +170,7 @@ interface CodeProps {
|
||||
hideInternalWand?: boolean
|
||||
}
|
||||
|
||||
export function Code({
|
||||
export const Code = memo(function Code({
|
||||
blockId,
|
||||
subBlockId,
|
||||
placeholder = 'Write JavaScript...',
|
||||
@@ -206,6 +206,8 @@ export function Code({
|
||||
const handleGeneratedContentRef = useRef<(generatedCode: string) => void>(() => {})
|
||||
const handleStreamChunkRef = useRef<(chunk: string) => void>(() => {})
|
||||
const hasEditedSinceFocusRef = useRef(false)
|
||||
const codeRef = useRef(code)
|
||||
codeRef.current = code
|
||||
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlockId)
|
||||
@@ -307,25 +309,18 @@ export function Code({
|
||||
? getDefaultValueString()
|
||||
: storeValue
|
||||
|
||||
const lastValidationStatus = useRef<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!onValidationChange) return
|
||||
|
||||
const nextStatus = shouldValidateJson ? isValidJson : true
|
||||
if (lastValidationStatus.current === nextStatus) {
|
||||
return
|
||||
}
|
||||
const isValid = !shouldValidateJson || isValidJson
|
||||
|
||||
lastValidationStatus.current = nextStatus
|
||||
|
||||
if (!shouldValidateJson) {
|
||||
onValidationChange(nextStatus)
|
||||
if (isValid) {
|
||||
onValidationChange(true)
|
||||
return
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
onValidationChange(nextStatus)
|
||||
onValidationChange(false)
|
||||
}, 150)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
@@ -337,7 +332,7 @@ export function Code({
|
||||
}
|
||||
|
||||
handleStreamChunkRef.current = (chunk: string) => {
|
||||
setCode((prev) => prev + chunk)
|
||||
setCode((prev: string) => prev + chunk)
|
||||
}
|
||||
|
||||
handleGeneratedContentRef.current = (generatedCode: string) => {
|
||||
@@ -434,12 +429,12 @@ export function Code({
|
||||
`
|
||||
document.body.appendChild(tempContainer)
|
||||
|
||||
lines.forEach((line) => {
|
||||
lines.forEach((line: string) => {
|
||||
const lineDiv = document.createElement('div')
|
||||
|
||||
if (line.includes('<') && line.includes('>')) {
|
||||
const parts = line.split(/(<[^>]+>)/g)
|
||||
parts.forEach((part) => {
|
||||
parts.forEach((part: string) => {
|
||||
const span = document.createElement('span')
|
||||
span.textContent = part
|
||||
lineDiv.appendChild(span)
|
||||
@@ -472,7 +467,6 @@ export function Code({
|
||||
}
|
||||
}, [code])
|
||||
|
||||
// Event Handlers
|
||||
/**
|
||||
* Handles drag-and-drop events for inserting reference tags into the code editor.
|
||||
* @param e - The drag event
|
||||
@@ -500,7 +494,6 @@ export function Code({
|
||||
textarea.selectionStart = newCursorPosition
|
||||
textarea.selectionEnd = newCursorPosition
|
||||
|
||||
// Show tag dropdown after cursor is positioned
|
||||
setShowTags(true)
|
||||
if (data.connectionData?.sourceBlockId) {
|
||||
setActiveSourceBlockId(data.connectionData.sourceBlockId)
|
||||
@@ -559,44 +552,45 @@ export function Code({
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
/**
|
||||
* Determines whether a `<...>` segment should be highlighted as a reference.
|
||||
* @param part - The code segment to check
|
||||
* @returns True if the segment should be highlighted as a reference
|
||||
*/
|
||||
const shouldHighlightReference = (part: string): boolean => {
|
||||
if (!part.startsWith('<') || !part.endsWith('>')) {
|
||||
return false
|
||||
}
|
||||
const shouldHighlightReference = useCallback(
|
||||
(part: string): boolean => {
|
||||
if (!part.startsWith('<') || !part.endsWith('>')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isLikelyReferenceSegment(part)) {
|
||||
return false
|
||||
}
|
||||
if (!isLikelyReferenceSegment(part)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const split = splitReferenceSegment(part)
|
||||
if (!split) {
|
||||
return false
|
||||
}
|
||||
const split = splitReferenceSegment(part)
|
||||
if (!split) {
|
||||
return false
|
||||
}
|
||||
|
||||
const reference = split.reference
|
||||
const reference = split.reference
|
||||
|
||||
if (!accessiblePrefixes) {
|
||||
return true
|
||||
}
|
||||
if (!accessiblePrefixes) {
|
||||
return true
|
||||
}
|
||||
|
||||
const inner = reference.slice(1, -1)
|
||||
const [prefix] = inner.split('.')
|
||||
const normalizedPrefix = normalizeName(prefix)
|
||||
const inner = reference.slice(1, -1)
|
||||
const [prefix] = inner.split('.')
|
||||
const normalizedPrefix = normalizeName(prefix)
|
||||
|
||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||
return true
|
||||
}
|
||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return accessiblePrefixes.has(normalizedPrefix)
|
||||
}
|
||||
return accessiblePrefixes.has(normalizedPrefix)
|
||||
},
|
||||
[accessiblePrefixes]
|
||||
)
|
||||
|
||||
// Expose wand control handlers to parent via ref
|
||||
useImperativeHandle(
|
||||
wandControlRef,
|
||||
() => ({
|
||||
@@ -609,6 +603,62 @@ export function Code({
|
||||
[generateCodeStream, isPromptVisible, isAiStreaming]
|
||||
)
|
||||
|
||||
const highlightCode = useMemo(
|
||||
() => createHighlightFunction(effectiveLanguage, shouldHighlightReference),
|
||||
[effectiveLanguage, shouldHighlightReference]
|
||||
)
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(newCode: string) => {
|
||||
if (!isAiStreaming && !isPreview && !disabled && !readOnly) {
|
||||
hasEditedSinceFocusRef.current = true
|
||||
setCode(newCode)
|
||||
setStoreValue(newCode)
|
||||
|
||||
const textarea = editorRef.current?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
const pos = textarea.selectionStart
|
||||
setCursorPosition(pos)
|
||||
|
||||
const tagTrigger = checkTagTrigger(newCode, pos)
|
||||
setShowTags(tagTrigger.show)
|
||||
if (!tagTrigger.show) {
|
||||
setActiveSourceBlockId(null)
|
||||
}
|
||||
|
||||
const envVarTrigger = checkEnvVarTrigger(newCode, pos)
|
||||
setShowEnvVars(envVarTrigger.show)
|
||||
setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '')
|
||||
}
|
||||
}
|
||||
},
|
||||
[isAiStreaming, isPreview, disabled, readOnly, setStoreValue]
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement | HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowTags(false)
|
||||
setShowEnvVars(false)
|
||||
}
|
||||
if (isAiStreaming) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !hasEditedSinceFocusRef.current) {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
[isAiStreaming]
|
||||
)
|
||||
|
||||
const handleEditorFocus = useCallback(() => {
|
||||
hasEditedSinceFocusRef.current = false
|
||||
if (!isPreview && !disabled && !readOnly && codeRef.current.trim() === '') {
|
||||
setShowTags(true)
|
||||
setCursorPosition(0)
|
||||
}
|
||||
}, [isPreview, disabled, readOnly])
|
||||
|
||||
/**
|
||||
* Renders the line numbers, aligned with wrapped visual lines and highlighting the active line.
|
||||
* @returns Array of React elements representing the line numbers
|
||||
@@ -617,7 +667,7 @@ export function Code({
|
||||
const numbers: ReactElement[] = []
|
||||
let lineNumber = 1
|
||||
|
||||
visualLineHeights.forEach((height) => {
|
||||
visualLineHeights.forEach((height: number) => {
|
||||
const isActive = lineNumber === activeLineNumber
|
||||
numbers.push(
|
||||
<div
|
||||
@@ -724,50 +774,10 @@ export function Code({
|
||||
|
||||
<Editor
|
||||
value={code}
|
||||
onValueChange={(newCode) => {
|
||||
if (!isAiStreaming && !isPreview && !disabled && !readOnly) {
|
||||
hasEditedSinceFocusRef.current = true
|
||||
setCode(newCode)
|
||||
setStoreValue(newCode)
|
||||
|
||||
const textarea = editorRef.current?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
const pos = textarea.selectionStart
|
||||
setCursorPosition(pos)
|
||||
|
||||
const tagTrigger = checkTagTrigger(newCode, pos)
|
||||
setShowTags(tagTrigger.show)
|
||||
if (!tagTrigger.show) {
|
||||
setActiveSourceBlockId(null)
|
||||
}
|
||||
|
||||
const envVarTrigger = checkEnvVarTrigger(newCode, pos)
|
||||
setShowEnvVars(envVarTrigger.show)
|
||||
setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '')
|
||||
}
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowTags(false)
|
||||
setShowEnvVars(false)
|
||||
}
|
||||
if (isAiStreaming) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.key === 'z' && (e.metaKey || e.ctrlKey) && !hasEditedSinceFocusRef.current) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onFocus={() => {
|
||||
hasEditedSinceFocusRef.current = false
|
||||
// Show tag dropdown on focus when code is empty
|
||||
if (!isPreview && !disabled && !readOnly && code.trim() === '') {
|
||||
setShowTags(true)
|
||||
setCursorPosition(0)
|
||||
}
|
||||
}}
|
||||
highlight={createHighlightFunction(effectiveLanguage, shouldHighlightReference)}
|
||||
onValueChange={handleValueChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleEditorFocus}
|
||||
highlight={highlightCode}
|
||||
{...getCodeEditorProps({ isStreaming: isAiStreaming, isPreview, disabled })}
|
||||
/>
|
||||
|
||||
@@ -810,4 +820,4 @@ export function Code({
|
||||
</CodeEditor.Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
@@ -71,7 +72,7 @@ interface ComboBoxProps {
|
||||
dependsOn?: SubBlockConfig['dependsOn']
|
||||
}
|
||||
|
||||
export function ComboBox({
|
||||
export const ComboBox = memo(function ComboBox({
|
||||
options,
|
||||
defaultValue,
|
||||
blockId,
|
||||
@@ -112,7 +113,8 @@ export function ComboBox({
|
||||
)
|
||||
},
|
||||
[dependsOnFields, activeWorkflowId, blockId, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
),
|
||||
isEqual
|
||||
)
|
||||
|
||||
// State management
|
||||
@@ -281,34 +283,17 @@ export function ComboBox({
|
||||
setStoreInitialized(true)
|
||||
}, [])
|
||||
|
||||
// Check if current value is valid (exists in allowed options)
|
||||
const isValueValid = useMemo(() => {
|
||||
if (value === null || value === undefined) return false
|
||||
return evaluatedOptions.some((opt) => getOptionValue(opt) === value)
|
||||
}, [value, evaluatedOptions, getOptionValue])
|
||||
|
||||
// Set default value once store is initialized and permissions are loaded
|
||||
// Also reset if current value becomes invalid (e.g., provider was blocked)
|
||||
useEffect(() => {
|
||||
if (isPermissionLoading) return
|
||||
if (!storeInitialized) return
|
||||
if (defaultOptionValue === undefined) return
|
||||
|
||||
const needsDefault = value === null || value === undefined
|
||||
const needsReset = subBlockId === 'model' && value && !isValueValid
|
||||
|
||||
if (needsDefault || needsReset) {
|
||||
// Only set default when no value exists (initial block add)
|
||||
if (value === null || value === undefined) {
|
||||
setStoreValue(defaultOptionValue)
|
||||
}
|
||||
}, [
|
||||
storeInitialized,
|
||||
value,
|
||||
defaultOptionValue,
|
||||
setStoreValue,
|
||||
isPermissionLoading,
|
||||
subBlockId,
|
||||
isValueValid,
|
||||
])
|
||||
}, [storeInitialized, value, defaultOptionValue, setStoreValue, isPermissionLoading])
|
||||
|
||||
// Clear fetched options and hydrated option when dependencies change
|
||||
useEffect(() => {
|
||||
@@ -437,6 +422,18 @@ export function ComboBox({
|
||||
[reactFlowInstance]
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles combobox open state changes to trigger option fetching
|
||||
*/
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
void fetchOptionsIfNeeded()
|
||||
}
|
||||
},
|
||||
[fetchOptionsIfNeeded]
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the icon for the currently selected option
|
||||
*/
|
||||
@@ -466,6 +463,75 @@ export function ComboBox({
|
||||
)
|
||||
}, [inputValue, accessiblePrefixes, selectedOption, selectedOptionIcon])
|
||||
|
||||
const ctrlOnChangeRef = useRef<
|
||||
((e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void) | null
|
||||
>(null)
|
||||
const onDropRef = useRef<
|
||||
((e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void) | null
|
||||
>(null)
|
||||
const onDragOverRef = useRef<
|
||||
((e: React.DragEvent<HTMLTextAreaElement | HTMLInputElement>) => void) | null
|
||||
>(null)
|
||||
const inputRefFromController = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const comboboxOnChange = useCallback(
|
||||
(newValue: string) => {
|
||||
const matchedComboboxOption = comboboxOptions.find((option) => option.value === newValue)
|
||||
if (matchedComboboxOption) {
|
||||
setInputValue(matchedComboboxOption.label)
|
||||
} else {
|
||||
setInputValue(newValue)
|
||||
}
|
||||
|
||||
// Use controller's handler so env vars, tags, and DnD still work
|
||||
const syntheticEvent = {
|
||||
target: { value: newValue, selectionStart: newValue.length },
|
||||
} as React.ChangeEvent<HTMLInputElement>
|
||||
ctrlOnChangeRef.current?.(syntheticEvent)
|
||||
},
|
||||
[comboboxOptions, setInputValue]
|
||||
)
|
||||
|
||||
const comboboxInputProps = useMemo(
|
||||
() => ({
|
||||
onDrop: ((e: React.DragEvent<HTMLInputElement>) => {
|
||||
onDropRef.current?.(e)
|
||||
}) as (e: React.DragEvent<HTMLInputElement>) => void,
|
||||
onDragOver: ((e: React.DragEvent<HTMLInputElement>) => {
|
||||
onDragOverRef.current?.(e)
|
||||
}) as (e: React.DragEvent<HTMLInputElement>) => void,
|
||||
onWheel: handleWheel,
|
||||
autoComplete: 'off' as const,
|
||||
}),
|
||||
[handleWheel]
|
||||
)
|
||||
|
||||
// Stable onChange for SubBlockInputController
|
||||
const controllerOnChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (isPreview) {
|
||||
return
|
||||
}
|
||||
|
||||
const matchedOption = evaluatedOptions.find((option) => {
|
||||
if (typeof option === 'string') {
|
||||
return option === newValue
|
||||
}
|
||||
return option.id === newValue
|
||||
})
|
||||
|
||||
// If a matching option is found, store its ID; otherwise store the raw value
|
||||
// (allows expressions like <block.output> to be entered directly)
|
||||
const nextValue = matchedOption
|
||||
? typeof matchedOption === 'string'
|
||||
? matchedOption
|
||||
: matchedOption.id
|
||||
: newValue
|
||||
setStoreValue(nextValue)
|
||||
},
|
||||
[isPreview, evaluatedOptions, setStoreValue]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='relative w-full'>
|
||||
<SubBlockInputController
|
||||
@@ -473,76 +539,43 @@ export function ComboBox({
|
||||
subBlockId={subBlockId}
|
||||
config={config}
|
||||
value={propValue}
|
||||
onChange={(newValue) => {
|
||||
if (isPreview) {
|
||||
return
|
||||
}
|
||||
|
||||
const matchedOption = evaluatedOptions.find((option) => {
|
||||
if (typeof option === 'string') {
|
||||
return option === newValue
|
||||
}
|
||||
return option.id === newValue
|
||||
})
|
||||
|
||||
// If a matching option is found, store its ID; otherwise store the raw value
|
||||
// (allows expressions like <block.output> to be entered directly)
|
||||
const nextValue = matchedOption
|
||||
? typeof matchedOption === 'string'
|
||||
? matchedOption
|
||||
: matchedOption.id
|
||||
: newValue
|
||||
setStoreValue(nextValue)
|
||||
}}
|
||||
onChange={controllerOnChange}
|
||||
isPreview={isPreview}
|
||||
disabled={disabled}
|
||||
previewValue={previewValue}
|
||||
>
|
||||
{({ ref, onChange: ctrlOnChange, onDrop, onDragOver }) => (
|
||||
<Combobox
|
||||
options={comboboxOptions}
|
||||
value={inputValue}
|
||||
selectedValue={value ?? ''}
|
||||
onChange={(newValue) => {
|
||||
const matchedComboboxOption = comboboxOptions.find(
|
||||
(option) => option.value === newValue
|
||||
)
|
||||
if (matchedComboboxOption) {
|
||||
setInputValue(matchedComboboxOption.label)
|
||||
} else {
|
||||
setInputValue(newValue)
|
||||
}
|
||||
{({ ref, onChange: ctrlOnChange, onDrop, onDragOver }) => {
|
||||
// Update refs with latest handlers from render prop
|
||||
ctrlOnChangeRef.current = ctrlOnChange
|
||||
onDropRef.current = onDrop
|
||||
onDragOverRef.current = onDragOver
|
||||
// Store the input ref for passing to Combobox
|
||||
if (ref.current) {
|
||||
inputRefFromController.current = ref.current as HTMLInputElement
|
||||
}
|
||||
|
||||
// Use controller's handler so env vars, tags, and DnD still work
|
||||
const syntheticEvent = {
|
||||
target: { value: newValue, selectionStart: newValue.length },
|
||||
} as React.ChangeEvent<HTMLInputElement>
|
||||
ctrlOnChange(syntheticEvent)
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
editable
|
||||
overlayContent={overlayContent}
|
||||
inputRef={ref as React.RefObject<HTMLInputElement>}
|
||||
filterOptions
|
||||
searchable={config.searchable}
|
||||
className={cn('allow-scroll overflow-x-auto', selectedOptionIcon && 'pl-[28px]')}
|
||||
inputProps={{
|
||||
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,
|
||||
onDragOver: onDragOver as (e: React.DragEvent<HTMLInputElement>) => void,
|
||||
onWheel: handleWheel,
|
||||
autoComplete: 'off',
|
||||
}}
|
||||
isLoading={isLoadingOptions}
|
||||
error={fetchError}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
void fetchOptionsIfNeeded()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<Combobox
|
||||
options={comboboxOptions}
|
||||
value={inputValue}
|
||||
selectedValue={value ?? ''}
|
||||
onChange={comboboxOnChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
editable
|
||||
overlayContent={overlayContent}
|
||||
inputRef={ref as React.RefObject<HTMLInputElement>}
|
||||
filterOptions
|
||||
searchable={config.searchable}
|
||||
className={cn('allow-scroll overflow-x-auto', selectedOptionIcon && 'pl-[28px]')}
|
||||
inputProps={comboboxInputProps}
|
||||
isLoading={isLoadingOptions}
|
||||
error={fetchError}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</SubBlockInputController>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ interface DocumentSelectorProps {
|
||||
onDocumentSelect?: (documentId: string) => void
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export function DocumentSelector({
|
||||
@@ -24,9 +25,15 @@ export function DocumentSelector({
|
||||
onDocumentSelect,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentSelectorProps) {
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||
disabled,
|
||||
isPreview,
|
||||
previewContextValues,
|
||||
})
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const normalizedKnowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
|
||||
@@ -37,6 +37,7 @@ interface DocumentTagEntryProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: any
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export function DocumentTagEntry({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: DocumentTagEntryProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
@@ -74,8 +76,12 @@ export function DocumentTagEntry({
|
||||
disabled,
|
||||
})
|
||||
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseId = knowledgeBaseIdValue || null
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
: null
|
||||
|
||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||
@@ -131,11 +137,16 @@ export function DocumentTagEntry({
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag by ID (prevents removing the last tag)
|
||||
* Removes a tag by ID, or resets it if it's the last one
|
||||
*/
|
||||
const removeTag = (id: string) => {
|
||||
if (isReadOnly || tags.length === 1) return
|
||||
updateTags(tags.filter((t) => t.id !== id))
|
||||
if (isReadOnly) return
|
||||
if (tags.length === 1) {
|
||||
// Reset the last tag instead of removing it
|
||||
updateTags([createDefaultTag()])
|
||||
} else {
|
||||
updateTags(tags.filter((t) => t.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,6 +233,7 @@ export function DocumentTagEntry({
|
||||
|
||||
/**
|
||||
* Renders the tag header with name, badge, and action buttons
|
||||
* Shows tag name only when collapsed (as summary), generic label when expanded
|
||||
*/
|
||||
const renderTagHeader = (tag: DocumentTag, index: number) => (
|
||||
<div
|
||||
@@ -230,9 +242,11 @@ export function DocumentTagEntry({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{tag.tagName || `Tag ${index + 1}`}
|
||||
{tag.collapsed ? tag.tagName || `Tag ${index + 1}` : `Tag ${index + 1}`}
|
||||
</span>
|
||||
{tag.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>}
|
||||
{tag.collapsed && tag.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[tag.fieldType] || 'Text'}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
@@ -247,7 +261,7 @@ export function DocumentTagEntry({
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeTag(tag.id)}
|
||||
disabled={isReadOnly || tags.length === 1}
|
||||
disabled={isReadOnly}
|
||||
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
@@ -341,7 +355,7 @@ export function DocumentTagEntry({
|
||||
|
||||
const tagOptions: ComboboxOption[] = selectableTags.map((t) => ({
|
||||
value: t.displayName,
|
||||
label: `${t.displayName} (${FIELD_TYPE_LABELS[t.fieldType] || 'Text'})`,
|
||||
label: t.displayName,
|
||||
}))
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { Combobox, type ComboboxOption } from '@/components/emcn/components'
|
||||
import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility'
|
||||
@@ -68,7 +69,7 @@ interface DropdownProps {
|
||||
* - Special handling for dataMode subblock to convert between JSON and structured formats
|
||||
* - Integrates with the workflow state management system
|
||||
*/
|
||||
export function Dropdown({
|
||||
export const Dropdown = memo(function Dropdown({
|
||||
options,
|
||||
defaultValue,
|
||||
blockId,
|
||||
@@ -110,7 +111,8 @@ export function Dropdown({
|
||||
)
|
||||
},
|
||||
[dependsOnFields, activeWorkflowId, blockId, canonicalIndex, canonicalModeOverrides]
|
||||
)
|
||||
),
|
||||
isEqual
|
||||
)
|
||||
|
||||
const [storeInitialized, setStoreInitialized] = useState(false)
|
||||
@@ -161,6 +163,18 @@ export function Dropdown({
|
||||
}
|
||||
}, [fetchOptions, blockId, subBlockId, isPreview, disabled])
|
||||
|
||||
/**
|
||||
* Handles combobox open state changes to trigger option fetching
|
||||
*/
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
void fetchOptionsIfNeeded()
|
||||
}
|
||||
},
|
||||
[fetchOptionsIfNeeded]
|
||||
)
|
||||
|
||||
const evaluatedOptions = useMemo(() => {
|
||||
return typeof options === 'function' ? options() : options
|
||||
}, [options])
|
||||
@@ -471,11 +485,7 @@ export function Dropdown({
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
editable={false}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
void fetchOptionsIfNeeded()
|
||||
}
|
||||
}}
|
||||
onOpenChange={handleOpenChange}
|
||||
overlayContent={multiSelectOverlay}
|
||||
multiSelect={multiSelect}
|
||||
isLoading={isLoadingOptions}
|
||||
@@ -484,4 +494,4 @@ export function Dropdown({
|
||||
searchPlaceholder='Search...'
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -40,6 +40,7 @@ interface KnowledgeTagFiltersProps {
|
||||
disabled?: boolean
|
||||
isPreview?: boolean
|
||||
previewValue?: string | null
|
||||
previewContextValues?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,14 +61,19 @@ export function KnowledgeTagFilters({
|
||||
disabled = false,
|
||||
isPreview = false,
|
||||
previewValue,
|
||||
previewContextValues,
|
||||
}: KnowledgeTagFiltersProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
|
||||
const emitTagSelection = useTagSelection(blockId, subBlock.id)
|
||||
const valueInputRefs = useRef<Record<string, HTMLInputElement>>({})
|
||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||
|
||||
const [knowledgeBaseIdValue] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseId = knowledgeBaseIdValue || null
|
||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
||||
const knowledgeBaseId =
|
||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||
? knowledgeBaseIdValue
|
||||
: null
|
||||
|
||||
const { tagDefinitions, isLoading } = useKnowledgeBaseTagDefinitions(knowledgeBaseId)
|
||||
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
|
||||
@@ -123,11 +129,16 @@ export function KnowledgeTagFilters({
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a filter by ID (prevents removing the last filter)
|
||||
* Removes a filter by ID, or resets it if it's the last one
|
||||
*/
|
||||
const removeFilter = (id: string) => {
|
||||
if (isReadOnly || filters.length === 1) return
|
||||
updateFilters(filters.filter((f) => f.id !== id))
|
||||
if (isReadOnly) return
|
||||
if (filters.length === 1) {
|
||||
// Reset the last filter instead of removing it
|
||||
updateFilters([createDefaultFilter()])
|
||||
} else {
|
||||
updateFilters(filters.filter((f) => f.id !== id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +226,7 @@ export function KnowledgeTagFilters({
|
||||
|
||||
/**
|
||||
* Renders the filter header with name, badge, and action buttons
|
||||
* Shows tag name only when collapsed (as summary), generic label when expanded
|
||||
*/
|
||||
const renderFilterHeader = (filter: TagFilter, index: number) => (
|
||||
<div
|
||||
@@ -223,9 +235,11 @@ export function KnowledgeTagFilters({
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[14px] text-[var(--text-tertiary)]'>
|
||||
{filter.tagName || `Filter ${index + 1}`}
|
||||
{filter.collapsed ? filter.tagName || `Filter ${index + 1}` : `Filter ${index + 1}`}
|
||||
</span>
|
||||
{filter.tagName && <Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>}
|
||||
{filter.collapsed && filter.tagName && (
|
||||
<Badge size='sm'>{FIELD_TYPE_LABELS[filter.fieldType] || 'Text'}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant='ghost' onClick={addFilter} disabled={isReadOnly} className='h-auto p-0'>
|
||||
@@ -235,7 +249,7 @@ export function KnowledgeTagFilters({
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => removeFilter(filter.id)}
|
||||
disabled={isReadOnly || filters.length === 1}
|
||||
disabled={isReadOnly}
|
||||
className='h-auto p-0 text-[var(--text-error)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
@@ -324,7 +338,7 @@ export function KnowledgeTagFilters({
|
||||
const renderFilterContent = (filter: TagFilter) => {
|
||||
const tagOptions: ComboboxOption[] = tagDefinitions.map((tag) => ({
|
||||
value: tag.displayName,
|
||||
label: `${tag.displayName} (${FIELD_TYPE_LABELS[tag.fieldType] || 'Text'})`,
|
||||
label: tag.displayName,
|
||||
}))
|
||||
|
||||
const operators = getOperatorsForFieldType(filter.fieldType)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react'
|
||||
import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
@@ -225,14 +226,18 @@ export function MessagesInput({
|
||||
[wandHook]
|
||||
)
|
||||
|
||||
/**
|
||||
* Initialize local state from stored or preview value
|
||||
*/
|
||||
const localMessagesRef = useRef(localMessages)
|
||||
localMessagesRef.current = localMessages
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreview && previewValue && Array.isArray(previewValue)) {
|
||||
setLocalMessages(previewValue)
|
||||
if (!isEqual(localMessagesRef.current, previewValue)) {
|
||||
setLocalMessages(previewValue)
|
||||
}
|
||||
} else if (messages && Array.isArray(messages) && messages.length > 0) {
|
||||
setLocalMessages(messages)
|
||||
if (!isEqual(localMessagesRef.current, messages)) {
|
||||
setLocalMessages(messages)
|
||||
}
|
||||
}
|
||||
}, [isPreview, previewValue, messages])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
||||
import { Check, Copy, Wand2 } from 'lucide-react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { Input } from '@/components/emcn'
|
||||
@@ -62,7 +62,7 @@ interface ShortInputProps {
|
||||
* - Copy to clipboard functionality
|
||||
* - Integrates with ReactFlow for zoom control
|
||||
*/
|
||||
export function ShortInput({
|
||||
export const ShortInput = memo(function ShortInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
placeholder,
|
||||
@@ -445,4 +445,4 @@ export function ShortInput({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import 'prismjs/components/prism-json'
|
||||
@@ -81,6 +81,8 @@ const createDefaultField = (): Field => ({
|
||||
*/
|
||||
const validateFieldName = (name: string): string => name.replace(/[\x00-\x1F"\\]/g, '').trim()
|
||||
|
||||
const jsonHighlight = (code: string): string => highlight(code, languages.json, 'json')
|
||||
|
||||
export function FieldFormat({
|
||||
blockId,
|
||||
subBlockId,
|
||||
@@ -138,17 +140,50 @@ export function FieldFormat({
|
||||
setStoreValue(fields.filter((field) => field.id !== id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific field property
|
||||
*/
|
||||
const updateField = (id: string, field: keyof Field, value: any) => {
|
||||
if (isReadOnly) return
|
||||
const storeValueRef = useRef(storeValue)
|
||||
storeValueRef.current = storeValue
|
||||
|
||||
const updatedValue =
|
||||
field === 'name' && typeof value === 'string' ? validateFieldName(value) : value
|
||||
const isReadOnlyRef = useRef(isReadOnly)
|
||||
isReadOnlyRef.current = isReadOnly
|
||||
|
||||
setStoreValue(fields.map((f) => (f.id === id ? { ...f, [field]: updatedValue } : f)))
|
||||
}
|
||||
const setStoreValueRef = useRef(setStoreValue)
|
||||
setStoreValueRef.current = setStoreValue
|
||||
|
||||
const updateField = useCallback(
|
||||
(id: string, fieldKey: keyof Field, fieldValue: Field[keyof Field]) => {
|
||||
if (isReadOnlyRef.current) return
|
||||
|
||||
const updatedValue =
|
||||
fieldKey === 'name' && typeof fieldValue === 'string'
|
||||
? validateFieldName(fieldValue)
|
||||
: fieldValue
|
||||
|
||||
const currentStoreValue = storeValueRef.current
|
||||
const currentFields: Field[] =
|
||||
Array.isArray(currentStoreValue) && currentStoreValue.length > 0
|
||||
? currentStoreValue
|
||||
: [createDefaultField()]
|
||||
|
||||
setStoreValueRef.current(
|
||||
currentFields.map((f) => (f.id === id ? { ...f, [fieldKey]: updatedValue } : f))
|
||||
)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const editorValueChangeHandlersRef = useRef<Record<string, (newValue: string) => void>>({})
|
||||
|
||||
const getEditorValueChangeHandler = useCallback(
|
||||
(fieldId: string): ((newValue: string) => void) => {
|
||||
if (!editorValueChangeHandlersRef.current[fieldId]) {
|
||||
editorValueChangeHandlersRef.current[fieldId] = (newValue: string) => {
|
||||
updateField(fieldId, 'value', newValue)
|
||||
}
|
||||
}
|
||||
return editorValueChangeHandlersRef.current[fieldId]
|
||||
},
|
||||
[updateField]
|
||||
)
|
||||
|
||||
/**
|
||||
* Toggles the collapsed state of a field
|
||||
@@ -222,15 +257,14 @@ export function FieldFormat({
|
||||
placeholder={placeholder}
|
||||
disabled={isReadOnly}
|
||||
autoComplete='off'
|
||||
className={cn('allow-scroll w-full overflow-auto', inputClassName)}
|
||||
style={{ overflowX: 'auto' }}
|
||||
className={cn('allow-scroll w-full overflow-x-auto overflow-y-hidden', inputClassName)}
|
||||
/>
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el) nameOverlayRefs.current[field.id] = el
|
||||
}}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
|
||||
style={{ overflowX: 'auto' }}
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
>
|
||||
<div
|
||||
className='w-full whitespace-pre'
|
||||
@@ -359,12 +393,8 @@ export function FieldFormat({
|
||||
</Code.Placeholder>
|
||||
<Editor
|
||||
value={fieldValue}
|
||||
onValueChange={(newValue) => {
|
||||
if (!isReadOnly) {
|
||||
updateField(field.id, 'value', newValue)
|
||||
}
|
||||
}}
|
||||
highlight={(code) => highlight(code, languages.json, 'json')}
|
||||
onValueChange={getEditorValueChangeHandler(field.id)}
|
||||
highlight={jsonHighlight}
|
||||
disabled={isReadOnly}
|
||||
{...getCodeEditorProps({ disabled: isReadOnly })}
|
||||
/>
|
||||
@@ -398,12 +428,8 @@ export function FieldFormat({
|
||||
</Code.Placeholder>
|
||||
<Editor
|
||||
value={fieldValue}
|
||||
onValueChange={(newValue) => {
|
||||
if (!isReadOnly) {
|
||||
updateField(field.id, 'value', newValue)
|
||||
}
|
||||
}}
|
||||
highlight={(code) => highlight(code, languages.json, 'json')}
|
||||
onValueChange={getEditorValueChangeHandler(field.id)}
|
||||
highlight={jsonHighlight}
|
||||
disabled={isReadOnly}
|
||||
{...getCodeEditorProps({ disabled: isReadOnly })}
|
||||
/>
|
||||
@@ -439,12 +465,8 @@ export function FieldFormat({
|
||||
</Code.Placeholder>
|
||||
<Editor
|
||||
value={fieldValue}
|
||||
onValueChange={(newValue) => {
|
||||
if (!isReadOnly) {
|
||||
updateField(field.id, 'value', newValue)
|
||||
}
|
||||
}}
|
||||
highlight={(code) => highlight(code, languages.json, 'json')}
|
||||
onValueChange={getEditorValueChangeHandler(field.id)}
|
||||
highlight={jsonHighlight}
|
||||
disabled={isReadOnly}
|
||||
{...getCodeEditorProps({ disabled: isReadOnly })}
|
||||
/>
|
||||
@@ -476,15 +498,14 @@ export function FieldFormat({
|
||||
placeholder={valuePlaceholder}
|
||||
disabled={isReadOnly}
|
||||
autoComplete='off'
|
||||
className={cn('allow-scroll w-full overflow-auto', inputClassName)}
|
||||
style={{ overflowX: 'auto' }}
|
||||
className={cn('allow-scroll w-full overflow-x-auto overflow-y-hidden', inputClassName)}
|
||||
/>
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el) overlayRefs.current[field.id] = el
|
||||
}}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'
|
||||
style={{ overflowX: 'auto' }}
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
>
|
||||
<div
|
||||
className='w-full whitespace-pre'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Loader2, WrenchIcon, XIcon } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Code,
|
||||
FileSelectorInput,
|
||||
FileUpload,
|
||||
FolderSelectorInput,
|
||||
LongInput,
|
||||
ProjectSelectorInput,
|
||||
SheetSelectorInput,
|
||||
@@ -45,7 +46,9 @@ import {
|
||||
TimeInput,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
|
||||
import { DocumentSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-selector/document-selector'
|
||||
import { DocumentTagEntry } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry'
|
||||
import { KnowledgeBaseSelector } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-base-selector/knowledge-base-selector'
|
||||
import { KnowledgeTagFilters } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters'
|
||||
import {
|
||||
type CustomTool,
|
||||
CustomToolModal,
|
||||
@@ -75,6 +78,13 @@ import {
|
||||
isPasswordParameter,
|
||||
type ToolParameterConfig,
|
||||
} from '@/tools/params'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
buildPreviewContextValues,
|
||||
type CanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
type SubBlockCondition,
|
||||
} from '@/tools/params-resolver'
|
||||
|
||||
const logger = createLogger('ToolInput')
|
||||
|
||||
@@ -304,6 +314,42 @@ function SheetSelectorSyncWrapper({
|
||||
)
|
||||
}
|
||||
|
||||
function FolderSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<FolderSelectorInput
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'folder-selector' as const,
|
||||
title: paramId,
|
||||
serviceId: uiComponent.serviceId,
|
||||
requiredScopes: uiComponent.requiredScopes || [],
|
||||
placeholder: uiComponent.placeholder,
|
||||
dependsOn: uiComponent.dependsOn,
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeBaseSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
@@ -342,6 +388,7 @@ function DocumentSelectorSyncWrapper({
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
@@ -349,6 +396,7 @@ function DocumentSelectorSyncWrapper({
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
@@ -361,6 +409,67 @@ function DocumentSelectorSyncWrapper({
|
||||
dependsOn: ['knowledgeBaseId'],
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentTagEntrySyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<DocumentTagEntry
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'document-tag-entry',
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function KnowledgeTagFiltersSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
previewContextValues,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
disabled: boolean
|
||||
previewContextValues?: Record<string, any>
|
||||
}) {
|
||||
return (
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<KnowledgeTagFilters
|
||||
blockId={blockId}
|
||||
subBlock={{
|
||||
id: paramId,
|
||||
type: 'knowledge-tag-filters',
|
||||
}}
|
||||
disabled={disabled}
|
||||
previewContextValues={previewContextValues}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
@@ -497,11 +606,15 @@ function CheckboxListSyncWrapper({
|
||||
}
|
||||
|
||||
function ComboboxSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
disabled,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
@@ -512,13 +625,15 @@ function ComboboxSyncWrapper({
|
||||
)
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select option'}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select option'}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -597,6 +712,8 @@ function SlackSelectorSyncWrapper({
|
||||
}
|
||||
|
||||
function WorkflowSelectorSyncWrapper({
|
||||
blockId,
|
||||
paramId,
|
||||
value,
|
||||
onChange,
|
||||
uiComponent,
|
||||
@@ -604,6 +721,8 @@ function WorkflowSelectorSyncWrapper({
|
||||
workspaceId,
|
||||
currentWorkflowId,
|
||||
}: {
|
||||
blockId: string
|
||||
paramId: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
uiComponent: any
|
||||
@@ -623,15 +742,17 @@ function WorkflowSelectorSyncWrapper({
|
||||
}))
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select workflow'}
|
||||
disabled={disabled || isLoading}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
/>
|
||||
<GenericSyncWrapper blockId={blockId} paramId={paramId} value={value} onChange={onChange}>
|
||||
<Combobox
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={uiComponent.placeholder || 'Select workflow'}
|
||||
disabled={disabled || isLoading}
|
||||
searchable
|
||||
searchPlaceholder='Search workflows...'
|
||||
/>
|
||||
</GenericSyncWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -877,7 +998,7 @@ function createToolIcon(bgColor: string, IconComponent: any) {
|
||||
* - Allows drag-and-drop reordering of selected tools
|
||||
* - Supports tool usage control (auto/force/none) for compatible LLM providers
|
||||
*/
|
||||
export function ToolInput({
|
||||
export const ToolInput = memo(function ToolInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview = false,
|
||||
@@ -1792,57 +1913,13 @@ export function ToolInput({
|
||||
return toolParams?.toolConfig?.oauth
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates parameter conditions to determine if a parameter should be visible.
|
||||
*
|
||||
* @remarks
|
||||
* Supports field value matching with arrays, negation via `not`, and
|
||||
* compound conditions via `and`. Used for conditional parameter visibility.
|
||||
*
|
||||
* @param param - The parameter configuration with optional condition
|
||||
* @param tool - The current tool instance with its parameter values
|
||||
* @returns `true` if the parameter should be shown based on its condition
|
||||
*/
|
||||
const evaluateParameterCondition = (param: any, tool: StoredTool): boolean => {
|
||||
if (!('uiComponent' in param) || !param.uiComponent?.condition) return true
|
||||
|
||||
const condition = param.uiComponent.condition
|
||||
const currentValues: Record<string, any> = {
|
||||
operation: tool.operation,
|
||||
...tool.params,
|
||||
}
|
||||
|
||||
const fieldValue = currentValues[condition.field]
|
||||
let result = false
|
||||
|
||||
if (Array.isArray(condition.value)) {
|
||||
result = condition.value.includes(fieldValue)
|
||||
} else {
|
||||
result = fieldValue === condition.value
|
||||
}
|
||||
|
||||
if (condition.not) {
|
||||
result = !result
|
||||
}
|
||||
|
||||
if (condition.and) {
|
||||
const andFieldValue = currentValues[condition.and.field]
|
||||
let andResult = false
|
||||
|
||||
if (Array.isArray(condition.and.value)) {
|
||||
andResult = condition.and.value.includes(andFieldValue)
|
||||
} else {
|
||||
andResult = andFieldValue === condition.and.value
|
||||
}
|
||||
|
||||
if (condition.and.not) {
|
||||
andResult = !andResult
|
||||
}
|
||||
|
||||
result = result && andResult
|
||||
}
|
||||
|
||||
return result
|
||||
const currentValues: Record<string, any> = { operation: tool.operation, ...tool.params }
|
||||
return evaluateSubBlockCondition(
|
||||
param.uiComponent.condition as SubBlockCondition,
|
||||
currentValues
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1961,7 +2038,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
selectorType='channel-selector'
|
||||
/>
|
||||
)
|
||||
@@ -1975,7 +2052,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
selectorType='user-selector'
|
||||
/>
|
||||
)
|
||||
@@ -1995,7 +2072,7 @@ export function ToolInput({
|
||||
}}
|
||||
onProjectSelect={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2020,7 +2097,7 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2033,7 +2110,20 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams as any}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'folder-selector':
|
||||
return (
|
||||
<FolderSelectorSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2052,6 +2142,8 @@ export function ToolInput({
|
||||
case 'combobox':
|
||||
return (
|
||||
<ComboboxSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
@@ -2110,6 +2202,8 @@ export function ToolInput({
|
||||
case 'workflow-selector':
|
||||
return (
|
||||
<WorkflowSelectorSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
@@ -2167,6 +2261,31 @@ export function ToolInput({
|
||||
onChange={onChange}
|
||||
uiComponent={uiComponent}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'document-tag-entry':
|
||||
return (
|
||||
<DocumentTagEntrySyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'knowledge-tag-filters':
|
||||
return (
|
||||
<KnowledgeTagFiltersSyncWrapper
|
||||
blockId={blockId}
|
||||
paramId={param.id}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
previewContextValues={currentToolParams}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2225,9 +2344,27 @@ export function ToolInput({
|
||||
// Get tool parameters using the new utility with block type for UI components
|
||||
const toolParams =
|
||||
!isCustomTool && !isMcpTool && currentToolId
|
||||
? getToolParametersConfig(currentToolId, tool.type)
|
||||
? getToolParametersConfig(currentToolId, tool.type, {
|
||||
operation: tool.operation,
|
||||
...tool.params,
|
||||
})
|
||||
: null
|
||||
|
||||
// Build canonical index for proper dependency resolution
|
||||
const toolCanonicalIndex: CanonicalIndex | null = toolBlock?.subBlocks
|
||||
? buildCanonicalIndex(toolBlock.subBlocks)
|
||||
: null
|
||||
|
||||
// Build preview context with canonical resolution
|
||||
const toolContextValues = toolCanonicalIndex
|
||||
? buildPreviewContextValues(tool.params || {}, {
|
||||
blockType: tool.type,
|
||||
subBlocks: toolBlock!.subBlocks,
|
||||
canonicalIndex: toolCanonicalIndex,
|
||||
values: { operation: tool.operation, ...tool.params },
|
||||
})
|
||||
: tool.params || {}
|
||||
|
||||
// For custom tools, resolve from reference (new format) or use inline (legacy)
|
||||
const resolvedCustomTool = isCustomTool
|
||||
? resolveCustomToolFromReference(tool, customTools)
|
||||
@@ -2590,7 +2727,7 @@ export function ToolInput({
|
||||
{param.required && param.visibility === 'user-only' && (
|
||||
<span className='ml-1'>*</span>
|
||||
)}
|
||||
{(!param.required || param.visibility !== 'user-only') && (
|
||||
{param.visibility === 'user-or-llm' && (
|
||||
<span className='ml-[6px] text-[12px] text-[var(--text-tertiary)]'>
|
||||
(optional)
|
||||
</span>
|
||||
@@ -2603,7 +2740,7 @@ export function ToolInput({
|
||||
tool.params?.[param.id] || '',
|
||||
(value) => handleParamChange(toolIndex, param.id, value),
|
||||
toolIndex,
|
||||
tool.params || {}
|
||||
toolContextValues as Record<string, string>
|
||||
)
|
||||
) : (
|
||||
<ShortInput
|
||||
@@ -2682,4 +2819,4 @@ export function ToolInput({
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
isNonEmptyValue,
|
||||
@@ -97,47 +98,60 @@ export function useDependsOnGate(
|
||||
return rawValue
|
||||
}
|
||||
|
||||
// Get values for all dependency fields (both all and any)
|
||||
const dependencyValuesMap = useSubBlockStore((state) => {
|
||||
if (allDependsOnFields.length === 0) return {} as Record<string, unknown>
|
||||
const dependencySelector = useCallback(
|
||||
(state: ReturnType<typeof useSubBlockStore.getState>) => {
|
||||
if (allDependsOnFields.length === 0) return {} as Record<string, unknown>
|
||||
|
||||
// If previewContextValues are provided (e.g., tool parameters), use those first
|
||||
if (previewContextValues) {
|
||||
// If previewContextValues are provided (e.g., tool parameters), use those first
|
||||
if (previewContextValues) {
|
||||
const map: Record<string, unknown> = {}
|
||||
for (const key of allDependsOnFields) {
|
||||
const resolvedValue = resolveDependencyValue(
|
||||
key,
|
||||
previewContextValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
)
|
||||
map[key] = normalizeDependencyValue(resolvedValue)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
if (!activeWorkflowId) {
|
||||
const map: Record<string, unknown> = {}
|
||||
for (const key of allDependsOnFields) {
|
||||
map[key] = null
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
const blockValues = (workflowValues as any)[blockId] || {}
|
||||
const map: Record<string, unknown> = {}
|
||||
for (const key of allDependsOnFields) {
|
||||
const resolvedValue = resolveDependencyValue(
|
||||
key,
|
||||
previewContextValues,
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
)
|
||||
map[key] = normalizeDependencyValue(resolvedValue)
|
||||
}
|
||||
return map
|
||||
}
|
||||
},
|
||||
[
|
||||
allDependsOnFields,
|
||||
previewContextValues,
|
||||
activeWorkflowId,
|
||||
blockId,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides,
|
||||
]
|
||||
)
|
||||
|
||||
if (!activeWorkflowId) {
|
||||
const map: Record<string, unknown> = {}
|
||||
for (const key of allDependsOnFields) {
|
||||
map[key] = null
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const workflowValues = state.workflowValues[activeWorkflowId] || {}
|
||||
const blockValues = (workflowValues as any)[blockId] || {}
|
||||
const map: Record<string, unknown> = {}
|
||||
for (const key of allDependsOnFields) {
|
||||
const resolvedValue = resolveDependencyValue(
|
||||
key,
|
||||
blockValues,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides
|
||||
)
|
||||
map[key] = normalizeDependencyValue(resolvedValue)
|
||||
}
|
||||
return map
|
||||
})
|
||||
// Get values for all dependency fields (both all and any)
|
||||
// Use isEqual to prevent re-renders when dependency values haven't actually changed
|
||||
const dependencyValuesMap = useSubBlockStore(dependencySelector, isEqual)
|
||||
|
||||
const depsSatisfied = useMemo(() => {
|
||||
// Check all fields (AND logic) - all must be satisfied
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { getProviderFromModel } from '@/providers/utils'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
@@ -51,16 +52,12 @@ export function useSubBlockValue<T = any>(
|
||||
)
|
||||
)
|
||||
|
||||
// Keep a ref to the latest value to prevent unnecessary re-renders
|
||||
const valueRef = useRef<T | null>(null)
|
||||
|
||||
// Streaming refs
|
||||
const lastEmittedValueRef = useRef<T | null>(null)
|
||||
const streamingValueRef = useRef<T | null>(null)
|
||||
const wasStreamingRef = useRef<boolean>(false)
|
||||
|
||||
// Get value from subblock store, keyed by active workflow id
|
||||
// Optimized: use shallow equality comparison to prevent re-renders when other fields change
|
||||
const storeValue = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
@@ -70,11 +67,17 @@ export function useSubBlockValue<T = any>(
|
||||
},
|
||||
[activeWorkflowId, blockId, subBlockId]
|
||||
),
|
||||
(a, b) => isEqual(a, b) // Use deep equality to prevent re-renders for same values
|
||||
(a, b) => isEqual(a, b)
|
||||
)
|
||||
|
||||
// Check if we're in diff mode and get diff value if available
|
||||
const { isShowingDiff, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore()
|
||||
const { isShowingDiff, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore(
|
||||
useShallow((state) => ({
|
||||
isShowingDiff: state.isShowingDiff,
|
||||
hasActiveDiff: state.hasActiveDiff,
|
||||
baselineWorkflow: state.baselineWorkflow,
|
||||
}))
|
||||
)
|
||||
const isBaselineView = hasActiveDiff && !isShowingDiff
|
||||
const snapshotSubBlock =
|
||||
isBaselineView && baselineWorkflow
|
||||
@@ -101,7 +104,7 @@ export function useSubBlockValue<T = any>(
|
||||
// Compute the modelValue based on block type
|
||||
const modelValue = isProviderBasedBlock ? (modelSubBlockValue as string) : null
|
||||
|
||||
// Emit the value to socket/DB
|
||||
// Emit the value to socket/DB and update local store
|
||||
const emitValue = useCallback(
|
||||
(value: T) => {
|
||||
collaborativeSetSubblockValue(blockId, subBlockId, value)
|
||||
@@ -155,20 +158,6 @@ export function useSubBlockValue<T = any>(
|
||||
return
|
||||
}
|
||||
|
||||
// Update local store immediately for UI responsiveness (non-streaming)
|
||||
useSubBlockStore.setState((state) => ({
|
||||
workflowValues: {
|
||||
...state.workflowValues,
|
||||
[currentActiveWorkflowId]: {
|
||||
...state.workflowValues[currentActiveWorkflowId],
|
||||
[blockId]: {
|
||||
...state.workflowValues[currentActiveWorkflowId]?.[blockId],
|
||||
[subBlockId]: newValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Handle model changes for provider-based blocks - clear API key when provider changes (non-streaming)
|
||||
if (
|
||||
subBlockId === 'model' &&
|
||||
@@ -206,6 +195,8 @@ export function useSubBlockValue<T = any>(
|
||||
isStreaming,
|
||||
emitValue,
|
||||
isBaselineView,
|
||||
collaborativeSetSubblockValue,
|
||||
isProviderBasedBlock,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type JSX, type MouseEvent, memo, useRef, useState } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { AlertTriangle, ArrowLeftRight, ArrowUp } from 'lucide-react'
|
||||
import { Button, Input, Label, Tooltip } from '@/components/emcn/components'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
@@ -207,21 +208,21 @@ const renderLabel = (
|
||||
<Label className='flex items-center gap-[6px] whitespace-nowrap'>
|
||||
{config.title}
|
||||
{required && <span className='ml-0.5'>*</span>}
|
||||
{config.type === 'code' && config.language === 'json' && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<AlertTriangle
|
||||
className={cn(
|
||||
'h-4 w-4 cursor-pointer text-destructive',
|
||||
!isValidJson ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>Invalid JSON</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{config.type === 'code' &&
|
||||
config.language === 'json' &&
|
||||
!isValidJson &&
|
||||
!wandState?.isStreaming && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className='inline-flex'>
|
||||
<AlertTriangle className='h-3 w-3 flex-shrink-0 cursor-pointer text-destructive' />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
<p>Invalid JSON</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</Label>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
{showWand && (
|
||||
@@ -239,9 +240,11 @@ const renderLabel = (
|
||||
<Input
|
||||
ref={wandState.searchInputRef}
|
||||
value={wandState.isStreaming ? 'Generating...' : wandState.searchQuery}
|
||||
onChange={(e) => wandState.onSearchChange(e.target.value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
wandState.onSearchChange(e.target.value)
|
||||
}
|
||||
onBlur={wandState.onSearchBlur}
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
e.key === 'Enter' &&
|
||||
wandState.searchQuery.trim() &&
|
||||
@@ -262,11 +265,11 @@ const renderLabel = (
|
||||
<Button
|
||||
variant='tertiary'
|
||||
disabled={!wandState.searchQuery.trim() || wandState.isStreaming}
|
||||
onMouseDown={(e) => {
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onClick={(e) => {
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
wandState.onSearchSubmit()
|
||||
}}
|
||||
@@ -302,22 +305,27 @@ const renderLabel = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares props to prevent unnecessary re-renders.
|
||||
*
|
||||
* @remarks
|
||||
* Used with React.memo to optimize performance by skipping re-renders
|
||||
* when props haven't meaningfully changed.
|
||||
* Compares props for memo equality check.
|
||||
*
|
||||
* @param prevProps - Previous component props
|
||||
* @param nextProps - Next component props
|
||||
* @returns `true` if props are equal and re-render should be skipped
|
||||
*/
|
||||
const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): boolean => {
|
||||
const subBlockId = prevProps.config.id
|
||||
const prevValue = prevProps.subBlockValues?.[subBlockId]?.value
|
||||
const nextValue = nextProps.subBlockValues?.[subBlockId]?.value
|
||||
|
||||
const valueEqual = prevValue === nextValue || isEqual(prevValue, nextValue)
|
||||
|
||||
const configEqual =
|
||||
prevProps.config.id === nextProps.config.id && prevProps.config.type === nextProps.config.type
|
||||
|
||||
return (
|
||||
prevProps.blockId === nextProps.blockId &&
|
||||
prevProps.config === nextProps.config &&
|
||||
configEqual &&
|
||||
prevProps.isPreview === nextProps.isPreview &&
|
||||
prevProps.subBlockValues === nextProps.subBlockValues &&
|
||||
valueEqual &&
|
||||
prevProps.disabled === nextProps.disabled &&
|
||||
prevProps.fieldDiffStatus === nextProps.fieldDiffStatus &&
|
||||
prevProps.allowExpandInPreview === nextProps.allowExpandInPreview &&
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { BookOpen, Check, ChevronDown, ChevronUp, Pencil } from 'lucide-react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
@@ -33,6 +35,9 @@ import { usePanelEditorStore } from '@/stores/panel'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
|
||||
/** Stable empty object to avoid creating new references */
|
||||
const EMPTY_SUBBLOCK_VALUES = {} as Record<string, any>
|
||||
|
||||
/**
|
||||
* Icon component for rendering block icons.
|
||||
*
|
||||
@@ -58,7 +63,15 @@ export function Editor() {
|
||||
toggleConnectionsCollapsed,
|
||||
shouldFocusRename,
|
||||
setShouldFocusRename,
|
||||
} = usePanelEditorStore()
|
||||
} = usePanelEditorStore(
|
||||
useShallow((state) => ({
|
||||
currentBlockId: state.currentBlockId,
|
||||
connectionsHeight: state.connectionsHeight,
|
||||
toggleConnectionsCollapsed: state.toggleConnectionsCollapsed,
|
||||
shouldFocusRename: state.shouldFocusRename,
|
||||
setShouldFocusRename: state.setShouldFocusRename,
|
||||
}))
|
||||
)
|
||||
const currentWorkflow = useCurrentWorkflow()
|
||||
const currentBlock = currentBlockId ? currentWorkflow.getBlockById(currentBlockId) : null
|
||||
const blockConfig = currentBlock ? getBlock(currentBlock.type) : null
|
||||
@@ -86,15 +99,15 @@ export function Editor() {
|
||||
currentWorkflow.isSnapshotView
|
||||
)
|
||||
|
||||
// Subscribe to block's subblock values
|
||||
const blockSubBlockValues = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!activeWorkflowId || !currentBlockId) return {}
|
||||
return state.workflowValues[activeWorkflowId]?.[currentBlockId] || {}
|
||||
if (!activeWorkflowId || !currentBlockId) return EMPTY_SUBBLOCK_VALUES
|
||||
return state.workflowValues[activeWorkflowId]?.[currentBlockId] ?? EMPTY_SUBBLOCK_VALUES
|
||||
},
|
||||
[activeWorkflowId, currentBlockId]
|
||||
)
|
||||
),
|
||||
isEqual
|
||||
)
|
||||
|
||||
const subBlocksForCanonical = useMemo(() => {
|
||||
|
||||
@@ -43,13 +43,12 @@ export function useBlockConnections(blockId: string) {
|
||||
)
|
||||
|
||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
const workflowSubBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? (state.workflowValues[workflowId] ?? {}) : {}
|
||||
)
|
||||
|
||||
// Helper function to merge block subBlocks with live values from subblock store
|
||||
const getMergedSubBlocks = (sourceBlockId: string): Record<string, any> => {
|
||||
const base = blocks[sourceBlockId]?.subBlocks || {}
|
||||
const workflowSubBlockValues = workflowId
|
||||
? (useSubBlockStore.getState().workflowValues[workflowId] ?? {})
|
||||
: {}
|
||||
const live = workflowSubBlockValues?.[sourceBlockId] || {}
|
||||
const merged: Record<string, any> = { ...base }
|
||||
for (const [subId, liveVal] of Object.entries(live)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { usePanelEditorStore } from '@/stores/panel'
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,12 @@ interface UseConnectionsResizeProps {
|
||||
* @returns Object containing resize handler
|
||||
*/
|
||||
export function useConnectionsResize({ subBlocksRef }: UseConnectionsResizeProps) {
|
||||
const { connectionsHeight, setConnectionsHeight } = usePanelEditorStore()
|
||||
const { connectionsHeight, setConnectionsHeight } = usePanelEditorStore(
|
||||
useShallow((state) => ({
|
||||
connectionsHeight: state.connectionsHeight,
|
||||
setConnectionsHeight: state.setConnectionsHeight,
|
||||
}))
|
||||
)
|
||||
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const startYRef = useRef<number>(0)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { shallow } from 'zustand/shallow'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
@@ -11,27 +12,36 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
* @returns Block display properties (advanced mode, trigger mode)
|
||||
*/
|
||||
export function useEditorBlockProperties(blockId: string | null, isSnapshotView: boolean) {
|
||||
const normalBlocks = useWorkflowStore(useCallback((state) => state.blocks, []))
|
||||
const baselineBlocks = useWorkflowDiffStore(
|
||||
useCallback((state) => state.baselineWorkflow?.blocks || {}, [])
|
||||
const normalBlockProps = useWorkflowStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!blockId) return { advancedMode: false, triggerMode: false }
|
||||
const block = state.blocks?.[blockId]
|
||||
return {
|
||||
advancedMode: block?.advancedMode ?? false,
|
||||
triggerMode: block?.triggerMode ?? false,
|
||||
}
|
||||
},
|
||||
[blockId]
|
||||
),
|
||||
shallow
|
||||
)
|
||||
|
||||
const blockProperties = useMemo(() => {
|
||||
if (!blockId) {
|
||||
return {
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
}
|
||||
}
|
||||
const baselineBlockProps = useWorkflowDiffStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!blockId) return { advancedMode: false, triggerMode: false }
|
||||
const block = state.baselineWorkflow?.blocks?.[blockId]
|
||||
return {
|
||||
advancedMode: block?.advancedMode ?? false,
|
||||
triggerMode: block?.triggerMode ?? false,
|
||||
}
|
||||
},
|
||||
[blockId]
|
||||
),
|
||||
shallow
|
||||
)
|
||||
|
||||
const blocks = isSnapshotView ? baselineBlocks : normalBlocks
|
||||
const block = blocks?.[blockId]
|
||||
|
||||
return {
|
||||
advancedMode: block?.advancedMode ?? false,
|
||||
triggerMode: block?.triggerMode ?? false,
|
||||
}
|
||||
}, [blockId, isSnapshotView, normalBlocks, baselineBlocks])
|
||||
|
||||
return blockProperties
|
||||
// Use the appropriate props based on view mode
|
||||
return isSnapshotView ? baselineBlockProps : normalBlockProps
|
||||
}
|
||||
|
||||
@@ -53,22 +53,27 @@ const SUBFLOW_CONFIG = {
|
||||
* @returns Subflow editor state and handlers
|
||||
*/
|
||||
export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId: string | null) {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// State
|
||||
const [tempInputValue, setTempInputValue] = useState<string | null>(null)
|
||||
const [showTagDropdown, setShowTagDropdown] = useState(false)
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
|
||||
// Check if current block is a subflow
|
||||
const isSubflow =
|
||||
currentBlock && (currentBlock.type === 'loop' || currentBlock.type === 'parallel')
|
||||
|
||||
// Get subflow configuration
|
||||
const subflowConfig = isSubflow ? SUBFLOW_CONFIG[currentBlock.type as 'loop' | 'parallel'] : null
|
||||
const nodeConfig = isSubflow ? workflowStore[subflowConfig!.storeKey][currentBlockId!] : null
|
||||
|
||||
const nodeConfig = useWorkflowStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!isSubflow || !subflowConfig || !currentBlockId) return null
|
||||
return state[subflowConfig.storeKey][currentBlockId] ?? null
|
||||
},
|
||||
[isSubflow, subflowConfig, currentBlockId]
|
||||
)
|
||||
)
|
||||
|
||||
// Get block data for fallback values
|
||||
const blockData = isSubflow ? currentBlock?.data : null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { ArrowUp, Square } from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import {
|
||||
BubbleChatClose,
|
||||
BubbleChatPreview,
|
||||
@@ -49,7 +50,6 @@ import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/st
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { getWorkflowWithValues } from '@/stores/workflows'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('Panel')
|
||||
/**
|
||||
@@ -69,14 +69,22 @@ const logger = createLogger('Panel')
|
||||
*
|
||||
* @returns Panel on the right side of the workflow
|
||||
*/
|
||||
export function Panel() {
|
||||
export const Panel = memo(function Panel() {
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const panelRef = useRef<HTMLElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { activeTab, setActiveTab, panelWidth, _hasHydrated, setHasHydrated } = usePanelStore()
|
||||
const { activeTab, setActiveTab, panelWidth, _hasHydrated, setHasHydrated } = usePanelStore(
|
||||
useShallow((state) => ({
|
||||
activeTab: state.activeTab,
|
||||
setActiveTab: state.setActiveTab,
|
||||
panelWidth: state.panelWidth,
|
||||
_hasHydrated: state._hasHydrated,
|
||||
setHasHydrated: state.setHasHydrated,
|
||||
}))
|
||||
)
|
||||
const copilotRef = useRef<{
|
||||
createNewChat: () => void
|
||||
setInputValueAndFocus: (value: string) => void
|
||||
@@ -97,12 +105,18 @@ export function Panel() {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const { config: permissionConfig } = usePermissionConfig()
|
||||
const { isImporting, handleFileChange } = useImportWorkflow({ workspaceId })
|
||||
const { workflows, activeWorkflowId, duplicateWorkflow, hydration } = useWorkflowRegistry()
|
||||
const { workflows, activeWorkflowId, duplicateWorkflow, hydration } = useWorkflowRegistry(
|
||||
useShallow((state) => ({
|
||||
workflows: state.workflows,
|
||||
activeWorkflowId: state.activeWorkflowId,
|
||||
duplicateWorkflow: state.duplicateWorkflow,
|
||||
hydration: state.hydration,
|
||||
}))
|
||||
)
|
||||
const isRegistryLoading =
|
||||
hydration.phase === 'idle' ||
|
||||
hydration.phase === 'metadata-loading' ||
|
||||
hydration.phase === 'state-loading'
|
||||
const { blocks } = useWorkflowStore()
|
||||
const { handleAutoLayout: autoLayoutWithFitView } = useAutoLayout(activeWorkflowId || null)
|
||||
|
||||
// Delete workflow hook
|
||||
@@ -157,8 +171,18 @@ export function Panel() {
|
||||
}, [usageExceeded, handleRunWorkflow])
|
||||
|
||||
// Chat state
|
||||
const { isChatOpen, setIsChatOpen } = useChatStore()
|
||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore()
|
||||
const { isChatOpen, setIsChatOpen } = useChatStore(
|
||||
useShallow((state) => ({
|
||||
isChatOpen: state.isChatOpen,
|
||||
setIsChatOpen: state.setIsChatOpen,
|
||||
}))
|
||||
)
|
||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore(
|
||||
useShallow((state) => ({
|
||||
isOpen: state.isOpen,
|
||||
setIsOpen: state.setIsOpen,
|
||||
}))
|
||||
)
|
||||
|
||||
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
|
||||
|
||||
@@ -583,4 +607,4 @@ export function Panel() {
|
||||
<Variables />
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,12 @@ export function getProviderName(providerId: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two WorkflowBlock props to determine if a re-render should be skipped
|
||||
* Used as the comparison function for React.memo
|
||||
* Compares two WorkflowBlock props to determine if a re-render should be skipped.
|
||||
* Used as the comparison function for React.memo.
|
||||
*
|
||||
* Note: xPos and yPos are intentionally excluded since WorkflowBlock doesn't use
|
||||
* position props - ReactFlow handles positioning via CSS transforms. Including them
|
||||
* would cause unnecessary re-renders during drag (100+ times per drag operation).
|
||||
*
|
||||
* @param prevProps - Previous node props
|
||||
* @param nextProps - Next node props
|
||||
@@ -37,9 +41,7 @@ export function shouldSkipBlockRender(
|
||||
prevProps.data.subBlockValues === nextProps.data.subBlockValues &&
|
||||
prevProps.data.blockState === nextProps.data.blockState &&
|
||||
prevProps.selected === nextProps.selected &&
|
||||
prevProps.dragging === nextProps.dragging &&
|
||||
prevProps.xPos === nextProps.xPos &&
|
||||
prevProps.yPos === nextProps.yPos
|
||||
prevProps.dragging === nextProps.dragging
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { isEqual } from 'lodash'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { Badge, Tooltip } from '@/components/emcn'
|
||||
@@ -7,6 +8,7 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createMcpToolId } from '@/lib/mcp/utils'
|
||||
import { getProviderIdFromServiceId } from '@/lib/oauth'
|
||||
import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
@@ -28,11 +30,7 @@ import {
|
||||
shouldSkipBlockRender,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/utils'
|
||||
import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import {
|
||||
BLOCK_DIMENSIONS,
|
||||
HANDLE_POSITIONS,
|
||||
useBlockDimensions,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
|
||||
import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions'
|
||||
import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types'
|
||||
import { getDependsOnFields } from '@/blocks/utils'
|
||||
import { useKnowledgeBase } from '@/hooks/kb/use-knowledge'
|
||||
@@ -49,6 +47,9 @@ import { wouldCreateCycle } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowBlock')
|
||||
|
||||
/** Stable empty object to avoid creating new references */
|
||||
const EMPTY_SUBBLOCK_VALUES = {} as Record<string, any>
|
||||
|
||||
/**
|
||||
* Type guard for table row structure
|
||||
*/
|
||||
@@ -323,23 +324,7 @@ export const getDisplayValue = (value: unknown): string => {
|
||||
return stringValue.trim().length > 0 ? stringValue : '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single subblock row with title and optional value.
|
||||
* Automatically hydrates IDs to display names for all selector types.
|
||||
*/
|
||||
const SubBlockRow = ({
|
||||
title,
|
||||
value,
|
||||
subBlock,
|
||||
rawValue,
|
||||
workspaceId,
|
||||
workflowId,
|
||||
blockId,
|
||||
allSubBlockValues,
|
||||
displayAdvancedOptions,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides,
|
||||
}: {
|
||||
interface SubBlockRowProps {
|
||||
title: string
|
||||
value?: string
|
||||
subBlock?: SubBlockConfig
|
||||
@@ -351,7 +336,53 @@ const SubBlockRow = ({
|
||||
displayAdvancedOptions?: boolean
|
||||
canonicalIndex?: ReturnType<typeof buildCanonicalIndex>
|
||||
canonicalModeOverrides?: Record<string, 'basic' | 'advanced'>
|
||||
}) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares SubBlockRow props for memo equality check.
|
||||
*/
|
||||
const areSubBlockRowPropsEqual = (
|
||||
prevProps: SubBlockRowProps,
|
||||
nextProps: SubBlockRowProps
|
||||
): boolean => {
|
||||
const subBlockId = prevProps.subBlock?.id
|
||||
const prevValue = subBlockId ? prevProps.allSubBlockValues?.[subBlockId]?.value : undefined
|
||||
const nextValue = subBlockId ? nextProps.allSubBlockValues?.[subBlockId]?.value : undefined
|
||||
const valueEqual = prevValue === nextValue || isEqual(prevValue, nextValue)
|
||||
|
||||
return (
|
||||
prevProps.title === nextProps.title &&
|
||||
prevProps.value === nextProps.value &&
|
||||
prevProps.subBlock === nextProps.subBlock &&
|
||||
prevProps.rawValue === nextProps.rawValue &&
|
||||
prevProps.workspaceId === nextProps.workspaceId &&
|
||||
prevProps.workflowId === nextProps.workflowId &&
|
||||
prevProps.blockId === nextProps.blockId &&
|
||||
valueEqual &&
|
||||
prevProps.displayAdvancedOptions === nextProps.displayAdvancedOptions &&
|
||||
prevProps.canonicalIndex === nextProps.canonicalIndex &&
|
||||
prevProps.canonicalModeOverrides === nextProps.canonicalModeOverrides
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single subblock row with title and optional value.
|
||||
* Automatically hydrates IDs to display names for all selector types.
|
||||
* Memoized to prevent excessive re-renders when parent components update.
|
||||
*/
|
||||
const SubBlockRow = memo(function SubBlockRow({
|
||||
title,
|
||||
value,
|
||||
subBlock,
|
||||
rawValue,
|
||||
workspaceId,
|
||||
workflowId,
|
||||
blockId,
|
||||
allSubBlockValues,
|
||||
displayAdvancedOptions,
|
||||
canonicalIndex,
|
||||
canonicalModeOverrides,
|
||||
}: SubBlockRowProps) {
|
||||
const getStringValue = useCallback(
|
||||
(key?: string): string | undefined => {
|
||||
if (!key || !allSubBlockValues) return undefined
|
||||
@@ -489,21 +520,34 @@ const SubBlockRow = ({
|
||||
: `${baseUrl}/api/webhooks/trigger/${blockId}`
|
||||
}, [subBlock?.id, blockId, allSubBlockValues])
|
||||
|
||||
const allVariables = useVariablesStore((state) => state.variables)
|
||||
/**
|
||||
* Subscribe only to variables for this workflow to avoid re-renders from other workflows.
|
||||
* Uses isEqual for deep comparison since Object.fromEntries creates a new object each time.
|
||||
*/
|
||||
const workflowVariables = useVariablesStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!workflowId) return {}
|
||||
return Object.fromEntries(
|
||||
Object.entries(state.variables).filter(([, v]) => v.workflowId === workflowId)
|
||||
)
|
||||
},
|
||||
[workflowId]
|
||||
),
|
||||
isEqual
|
||||
)
|
||||
|
||||
const variablesDisplayValue = useMemo(() => {
|
||||
if (subBlock?.type !== 'variables-input' || !isVariableAssignmentsArray(rawValue)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const workflowVariables = Object.values(allVariables).filter(
|
||||
(v: any) => v.workflowId === workflowId
|
||||
)
|
||||
const variablesArray = Object.values(workflowVariables)
|
||||
|
||||
const names = rawValue
|
||||
.map((a) => {
|
||||
if (a.variableId) {
|
||||
const variable = workflowVariables.find((v: any) => v.id === a.variableId)
|
||||
const variable = variablesArray.find((v: any) => v.id === a.variableId)
|
||||
return variable?.name
|
||||
}
|
||||
if (a.variableName) return a.variableName
|
||||
@@ -515,7 +559,7 @@ const SubBlockRow = ({
|
||||
if (names.length === 1) return names[0]
|
||||
if (names.length === 2) return `${names[0]}, ${names[1]}`
|
||||
return `${names[0]}, ${names[1]} +${names.length - 2}`
|
||||
}, [subBlock?.type, rawValue, workflowId, allVariables])
|
||||
}, [subBlock?.type, rawValue, workflowVariables])
|
||||
|
||||
const isPasswordField = subBlock?.password === true
|
||||
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
|
||||
@@ -551,7 +595,7 @@ const SubBlockRow = ({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}, areSubBlockRowPropsEqual)
|
||||
|
||||
export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
id,
|
||||
@@ -629,18 +673,15 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
const isStarterBlock = type === 'starter'
|
||||
const isWebhookTriggerBlock = type === 'webhook' || type === 'generic_webhook'
|
||||
|
||||
/**
|
||||
* Subscribe to this block's subblock values to track changes for conditional rendering
|
||||
* of subblocks based on their conditions.
|
||||
*/
|
||||
const blockSubBlockValues = useSubBlockStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (!activeWorkflowId) return {}
|
||||
return state.workflowValues[activeWorkflowId]?.[id] || {}
|
||||
if (!activeWorkflowId) return EMPTY_SUBBLOCK_VALUES
|
||||
return state.workflowValues[activeWorkflowId]?.[id] ?? EMPTY_SUBBLOCK_VALUES
|
||||
},
|
||||
[activeWorkflowId, id]
|
||||
)
|
||||
),
|
||||
isEqual
|
||||
)
|
||||
const canonicalIndex = useMemo(() => buildCanonicalIndex(config.subBlocks), [config.subBlocks])
|
||||
const canonicalModeOverrides = currentStoreBlock?.data?.canonicalModes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import clsx from 'clsx'
|
||||
import { Scan } from 'lucide-react'
|
||||
@@ -33,7 +33,10 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
const logger = createLogger('WorkflowControls')
|
||||
|
||||
export function WorkflowControls() {
|
||||
/**
|
||||
* Floating controls for canvas mode, undo/redo, and fit-to-view.
|
||||
*/
|
||||
export const WorkflowControls = memo(function WorkflowControls() {
|
||||
const reactFlowInstance = useReactFlow()
|
||||
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
|
||||
const { mode, setMode } = useCanvasModeStore()
|
||||
@@ -42,7 +45,7 @@ export function WorkflowControls() {
|
||||
const updateSetting = useUpdateGeneralSetting()
|
||||
const isTerminalResizing = useTerminalStore((state) => state.isResizing)
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
const { data: session } = useSession()
|
||||
const userId = session?.user?.id || 'unknown'
|
||||
const stacks = useUndoRedoStore((s) => s.stacks)
|
||||
@@ -222,4 +225,4 @@ export function WorkflowControls() {
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
export {
|
||||
clearDragHighlights,
|
||||
computeClampedPositionUpdates,
|
||||
computeParentUpdateEntries,
|
||||
getClampedPositionForNode,
|
||||
isInEditableElement,
|
||||
resolveParentChildSelectionConflicts,
|
||||
validateTriggerPaste,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers'
|
||||
export { useFloatBoundarySync, useFloatDrag, useFloatResize } from './float'
|
||||
export { useAccessibleReferencePrefixes } from './use-accessible-reference-prefixes'
|
||||
export { useAutoLayout } from './use-auto-layout'
|
||||
export { BLOCK_DIMENSIONS, useBlockDimensions } from './use-block-dimensions'
|
||||
export { useBlockDimensions } from './use-block-dimensions'
|
||||
export { useBlockOutputFields } from './use-block-output-fields'
|
||||
export { useBlockVisual } from './use-block-visual'
|
||||
export { useCanvasContextMenu } from './use-canvas-context-menu'
|
||||
export { type CurrentWorkflow, useCurrentWorkflow } from './use-current-workflow'
|
||||
export { calculateContainerDimensions, useNodeUtilities } from './use-node-utilities'
|
||||
export { useNodeUtilities } from './use-node-utilities'
|
||||
export { usePreventZoom } from './use-prevent-zoom'
|
||||
export { useScrollManagement } from './use-scroll-management'
|
||||
export { useShiftSelectionLock } from './use-shift-selection-lock'
|
||||
export { useWand, type WandConfig } from './use-wand'
|
||||
export { useWorkflowExecution } from './use-workflow-execution'
|
||||
|
||||
@@ -2,9 +2,6 @@ import { useEffect, useRef } from 'react'
|
||||
import { useUpdateNodeInternals } from 'reactflow'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
|
||||
interface BlockDimensions {
|
||||
width: number
|
||||
height: number
|
||||
|
||||
@@ -50,8 +50,18 @@ export function useBlockVisual({
|
||||
} = useBlockState(blockId, currentWorkflow, data)
|
||||
|
||||
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
|
||||
const activeTab = usePanelStore((state) => state.activeTab)
|
||||
const isEditorOpen = !isPreview && currentBlockId === blockId && activeTab === 'editor'
|
||||
|
||||
const isThisBlockInEditor = currentBlockId === blockId
|
||||
const activeTabIsEditor = usePanelStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
if (isPreview || !isThisBlockInEditor) return false
|
||||
return state.activeTab === 'editor'
|
||||
},
|
||||
[isPreview, isThisBlockInEditor]
|
||||
)
|
||||
)
|
||||
const isEditorOpen = !isPreview && isThisBlockInEditor && activeTabIsEditor
|
||||
|
||||
const lastRunPath = useExecutionStore((state) => state.lastRunPath)
|
||||
const runPathStatus = isPreview ? undefined : lastRunPath.get(blockId)
|
||||
|
||||
@@ -2,107 +2,15 @@ import { useCallback } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import {
|
||||
calculateContainerDimensions,
|
||||
clampPositionToContainer,
|
||||
estimateBlockDimensions,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('NodeUtilities')
|
||||
|
||||
/**
|
||||
* Estimates block dimensions based on block type.
|
||||
* Uses subblock count to estimate height for blocks that haven't been measured yet.
|
||||
*
|
||||
* @param blockType - The type of block (e.g., 'condition', 'agent')
|
||||
* @returns Estimated width and height for the block
|
||||
*/
|
||||
export function estimateBlockDimensions(blockType: string): { width: number; height: number } {
|
||||
const blockConfig = getBlock(blockType)
|
||||
const subBlockCount = blockConfig?.subBlocks?.length ?? 3
|
||||
// Many subblocks are conditionally rendered (advanced mode, provider-specific, etc.)
|
||||
// Use roughly half the config count as a reasonable estimate, capped between 3-7 rows
|
||||
const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7))
|
||||
const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0
|
||||
|
||||
const height =
|
||||
BLOCK_DIMENSIONS.HEADER_HEIGHT +
|
||||
BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING +
|
||||
(estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT
|
||||
|
||||
return {
|
||||
width: BLOCK_DIMENSIONS.FIXED_WIDTH,
|
||||
height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a position to keep a block fully inside a container's content area.
|
||||
* Content area starts after the header and padding, and ends before the right/bottom padding.
|
||||
*
|
||||
* @param position - Raw position relative to container origin
|
||||
* @param containerDimensions - Container width and height
|
||||
* @param blockDimensions - Block width and height
|
||||
* @returns Clamped position that keeps block inside content area
|
||||
*/
|
||||
export function clampPositionToContainer(
|
||||
position: { x: number; y: number },
|
||||
containerDimensions: { width: number; height: number },
|
||||
blockDimensions: { width: number; height: number }
|
||||
): { x: number; y: number } {
|
||||
const { width: containerWidth, height: containerHeight } = containerDimensions
|
||||
const { width: blockWidth, height: blockHeight } = blockDimensions
|
||||
|
||||
// Content area bounds (where blocks can be placed)
|
||||
const minX = CONTAINER_DIMENSIONS.LEFT_PADDING
|
||||
const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
|
||||
const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth
|
||||
const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight
|
||||
|
||||
return {
|
||||
x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))),
|
||||
y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates container dimensions based on child block positions.
|
||||
* Single source of truth for container sizing - ensures consistency between
|
||||
* live drag updates and final dimension calculations.
|
||||
*
|
||||
* @param childPositions - Array of child positions with their dimensions
|
||||
* @returns Calculated width and height for the container
|
||||
*/
|
||||
export function calculateContainerDimensions(
|
||||
childPositions: Array<{ x: number; y: number; width: number; height: number }>
|
||||
): { width: number; height: number } {
|
||||
if (childPositions.length === 0) {
|
||||
return {
|
||||
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
let maxRight = 0
|
||||
let maxBottom = 0
|
||||
|
||||
for (const child of childPositions) {
|
||||
maxRight = Math.max(maxRight, child.x + child.width)
|
||||
maxBottom = Math.max(maxBottom, child.y + child.height)
|
||||
}
|
||||
|
||||
const width = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
|
||||
)
|
||||
const height = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
|
||||
CONTAINER_DIMENSIONS.TOP_PADDING +
|
||||
maxBottom +
|
||||
CONTAINER_DIMENSIONS.BOTTOM_PADDING
|
||||
)
|
||||
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook providing utilities for node position, hierarchy, and dimension calculations
|
||||
*/
|
||||
@@ -138,7 +46,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer deterministic height published by the block component; fallback to estimate
|
||||
if (block.height) {
|
||||
return {
|
||||
width: BLOCK_DIMENSIONS.FIXED_WIDTH,
|
||||
@@ -146,7 +53,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// Use shared estimation utility for blocks without measured height
|
||||
return estimateBlockDimensions(block.type)
|
||||
},
|
||||
[blocks, isContainerType]
|
||||
@@ -230,8 +136,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
|
||||
const parentPos = getNodeAbsolutePosition(parentId)
|
||||
|
||||
// Child positions are stored relative to the content area (after header and padding)
|
||||
// Add these offsets when calculating absolute position
|
||||
const headerHeight = 50
|
||||
const leftPadding = 16
|
||||
const topPadding = 16
|
||||
@@ -314,7 +218,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
})
|
||||
.map((n) => ({
|
||||
loopId: n.id,
|
||||
// Return absolute position so callers can compute relative placement correctly
|
||||
loopPosition: getNodeAbsolutePosition(n.id),
|
||||
dimensions: {
|
||||
width: n.data?.width || CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
@@ -449,7 +352,6 @@ export function useNodeUtilities(blocks: Record<string, any>) {
|
||||
return absPos
|
||||
}
|
||||
|
||||
// Use known defaults per node type without type casting
|
||||
const isSubflow = node.type === 'subflowNode'
|
||||
const width = isSubflow
|
||||
? typeof node.data?.width === 'number'
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
interface UseShiftSelectionLockProps {
|
||||
isHandMode: boolean
|
||||
}
|
||||
|
||||
interface UseShiftSelectionLockResult {
|
||||
/** Whether a shift-selection is currently active (locked in until mouseup) */
|
||||
isShiftSelecting: boolean
|
||||
/** Handler to attach to canvas mousedown */
|
||||
handleCanvasMouseDown: (event: React.MouseEvent) => void
|
||||
/** Computed ReactFlow props based on current selection state */
|
||||
selectionProps: {
|
||||
selectionOnDrag: boolean
|
||||
panOnDrag: [number, number] | false
|
||||
selectionKeyCode: string | null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks shift-selection mode from mousedown to mouseup.
|
||||
* Prevents selection from canceling when shift is released mid-drag.
|
||||
*/
|
||||
export function useShiftSelectionLock({
|
||||
isHandMode,
|
||||
}: UseShiftSelectionLockProps): UseShiftSelectionLockResult {
|
||||
const [isShiftSelecting, setIsShiftSelecting] = useState(false)
|
||||
|
||||
const handleCanvasMouseDown = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!event.shiftKey) return
|
||||
|
||||
const target = event.target as HTMLElement | null
|
||||
const isPaneTarget = Boolean(target?.closest('.react-flow__pane, .react-flow__selectionpane'))
|
||||
|
||||
if (isPaneTarget && isHandMode) {
|
||||
setIsShiftSelecting(true)
|
||||
}
|
||||
|
||||
if (isPaneTarget) {
|
||||
event.preventDefault()
|
||||
window.getSelection()?.removeAllRanges()
|
||||
}
|
||||
},
|
||||
[isHandMode]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShiftSelecting) return
|
||||
|
||||
const handleMouseUp = () => setIsShiftSelecting(false)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => window.removeEventListener('mouseup', handleMouseUp)
|
||||
}, [isShiftSelecting])
|
||||
|
||||
const selectionProps = {
|
||||
selectionOnDrag: !isHandMode || isShiftSelecting,
|
||||
panOnDrag: (isHandMode && !isShiftSelecting ? [0, 1] : false) as [number, number] | false,
|
||||
selectionKeyCode: isShiftSelecting ? null : 'Shift',
|
||||
}
|
||||
|
||||
return { isShiftSelecting, handleCanvasMouseDown, selectionProps }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './auto-layout-utils'
|
||||
export * from './block-ring-utils'
|
||||
export * from './node-position-utils'
|
||||
export * from './workflow-canvas-helpers'
|
||||
export * from './workflow-execution-utils'
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
|
||||
/**
|
||||
* Estimates block dimensions based on block type.
|
||||
* Uses subblock count to estimate height for blocks that haven't been measured yet.
|
||||
*
|
||||
* @param blockType - The type of block (e.g., 'condition', 'agent')
|
||||
* @returns Estimated width and height for the block
|
||||
*/
|
||||
export function estimateBlockDimensions(blockType: string): { width: number; height: number } {
|
||||
const blockConfig = getBlock(blockType)
|
||||
const subBlockCount = blockConfig?.subBlocks?.length ?? 3
|
||||
const estimatedRows = Math.max(3, Math.min(Math.ceil(subBlockCount / 2), 7))
|
||||
const hasErrorRow = blockType !== 'starter' && blockType !== 'response' ? 1 : 0
|
||||
|
||||
const height =
|
||||
BLOCK_DIMENSIONS.HEADER_HEIGHT +
|
||||
BLOCK_DIMENSIONS.WORKFLOW_CONTENT_PADDING +
|
||||
(estimatedRows + hasErrorRow) * BLOCK_DIMENSIONS.WORKFLOW_ROW_HEIGHT
|
||||
|
||||
return {
|
||||
width: BLOCK_DIMENSIONS.FIXED_WIDTH,
|
||||
height: Math.max(height, BLOCK_DIMENSIONS.MIN_HEIGHT),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a position to keep a block fully inside a container's content area.
|
||||
* Content area starts after the header and padding, and ends before the right/bottom padding.
|
||||
*
|
||||
* @param position - Raw position relative to container origin
|
||||
* @param containerDimensions - Container width and height
|
||||
* @param blockDimensions - Block width and height
|
||||
* @returns Clamped position that keeps block inside content area
|
||||
*/
|
||||
export function clampPositionToContainer(
|
||||
position: { x: number; y: number },
|
||||
containerDimensions: { width: number; height: number },
|
||||
blockDimensions: { width: number; height: number }
|
||||
): { x: number; y: number } {
|
||||
const { width: containerWidth, height: containerHeight } = containerDimensions
|
||||
const { width: blockWidth, height: blockHeight } = blockDimensions
|
||||
|
||||
const minX = CONTAINER_DIMENSIONS.LEFT_PADDING
|
||||
const minY = CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING
|
||||
const maxX = containerWidth - CONTAINER_DIMENSIONS.RIGHT_PADDING - blockWidth
|
||||
const maxY = containerHeight - CONTAINER_DIMENSIONS.BOTTOM_PADDING - blockHeight
|
||||
|
||||
return {
|
||||
x: Math.max(minX, Math.min(position.x, Math.max(minX, maxX))),
|
||||
y: Math.max(minY, Math.min(position.y, Math.max(minY, maxY))),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates container dimensions based on child block positions.
|
||||
* Single source of truth for container sizing - ensures consistency between
|
||||
* live drag updates and final dimension calculations.
|
||||
*
|
||||
* @param childPositions - Array of child positions with their dimensions
|
||||
* @returns Calculated width and height for the container
|
||||
*/
|
||||
export function calculateContainerDimensions(
|
||||
childPositions: Array<{ x: number; y: number; width: number; height: number }>
|
||||
): { width: number; height: number } {
|
||||
if (childPositions.length === 0) {
|
||||
return {
|
||||
width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
height: CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
let maxRight = 0
|
||||
let maxBottom = 0
|
||||
|
||||
for (const child of childPositions) {
|
||||
maxRight = Math.max(maxRight, child.x + child.width)
|
||||
maxBottom = Math.max(maxBottom, child.y + child.height)
|
||||
}
|
||||
|
||||
const width = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_WIDTH,
|
||||
CONTAINER_DIMENSIONS.LEFT_PADDING + maxRight + CONTAINER_DIMENSIONS.RIGHT_PADDING
|
||||
)
|
||||
const height = Math.max(
|
||||
CONTAINER_DIMENSIONS.DEFAULT_HEIGHT,
|
||||
CONTAINER_DIMENSIONS.HEADER_HEIGHT +
|
||||
CONTAINER_DIMENSIONS.TOP_PADDING +
|
||||
maxBottom +
|
||||
CONTAINER_DIMENSIONS.BOTTOM_PADDING
|
||||
)
|
||||
|
||||
return { width, height }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Edge, Node } from 'reactflow'
|
||||
import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
|
||||
import { clampPositionToContainer } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/node-position-utils'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,22 +42,23 @@ import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/comp
|
||||
import { WorkflowControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
import {
|
||||
clearDragHighlights,
|
||||
computeClampedPositionUpdates,
|
||||
getClampedPositionForNode,
|
||||
isInEditableElement,
|
||||
resolveParentChildSelectionConflicts,
|
||||
useAutoLayout,
|
||||
useCanvasContextMenu,
|
||||
useCurrentWorkflow,
|
||||
useNodeUtilities,
|
||||
validateTriggerPaste,
|
||||
useShiftSelectionLock,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
|
||||
import { useCanvasContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu'
|
||||
import {
|
||||
calculateContainerDimensions,
|
||||
clampPositionToContainer,
|
||||
clearDragHighlights,
|
||||
computeClampedPositionUpdates,
|
||||
estimateBlockDimensions,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
|
||||
getClampedPositionForNode,
|
||||
isInEditableElement,
|
||||
resolveParentChildSelectionConflicts,
|
||||
validateTriggerPaste,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import { useSocket } from '@/app/workspace/providers/socket-provider'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
||||
@@ -235,6 +236,7 @@ const WorkflowContent = React.memo(() => {
|
||||
const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false)
|
||||
const canvasMode = useCanvasModeStore((state) => state.mode)
|
||||
const isHandMode = canvasMode === 'hand'
|
||||
const { handleCanvasMouseDown, selectionProps } = useShiftSelectionLock({ isHandMode })
|
||||
const [oauthModal, setOauthModal] = useState<{
|
||||
provider: OAuthProvider
|
||||
serviceId: string
|
||||
@@ -264,6 +266,9 @@ const WorkflowContent = React.memo(() => {
|
||||
preparePasteData,
|
||||
hasClipboard,
|
||||
clipboard,
|
||||
pendingSelection,
|
||||
setPendingSelection,
|
||||
clearPendingSelection,
|
||||
} = useWorkflowRegistry(
|
||||
useShallow((state) => ({
|
||||
workflows: state.workflows,
|
||||
@@ -274,6 +279,9 @@ const WorkflowContent = React.memo(() => {
|
||||
preparePasteData: state.preparePasteData,
|
||||
hasClipboard: state.hasClipboard,
|
||||
clipboard: state.clipboard,
|
||||
pendingSelection: state.pendingSelection,
|
||||
setPendingSelection: state.setPendingSelection,
|
||||
clearPendingSelection: state.clearPendingSelection,
|
||||
}))
|
||||
)
|
||||
|
||||
@@ -441,9 +449,6 @@ const WorkflowContent = React.memo(() => {
|
||||
new Map()
|
||||
)
|
||||
|
||||
/** Stores node IDs to select on next derivedNodes sync (for paste/duplicate operations). */
|
||||
const pendingSelectionRef = useRef<Set<string> | null>(null)
|
||||
|
||||
/** Re-applies diff markers when blocks change after socket rehydration. */
|
||||
const blocksRef = useRef(blocks)
|
||||
useEffect(() => {
|
||||
@@ -682,7 +687,7 @@ const WorkflowContent = React.memo(() => {
|
||||
autoConnectEdge?: Edge,
|
||||
triggerMode?: boolean
|
||||
) => {
|
||||
pendingSelectionRef.current = new Set([id])
|
||||
setPendingSelection([id])
|
||||
setSelectedEdges(new Map())
|
||||
|
||||
const blockData: Record<string, unknown> = { ...(data || {}) }
|
||||
@@ -719,7 +724,7 @@ const WorkflowContent = React.memo(() => {
|
||||
)
|
||||
usePanelEditorStore.getState().setCurrentBlockId(id)
|
||||
},
|
||||
[collaborativeBatchAddBlocks, setSelectedEdges]
|
||||
[collaborativeBatchAddBlocks, setSelectedEdges, setPendingSelection]
|
||||
)
|
||||
|
||||
const { activeBlockIds, pendingBlocks, isDebugging } = useExecutionStore(
|
||||
@@ -881,10 +886,7 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
|
||||
// Set pending selection before adding blocks - sync effect will apply it (accumulates for rapid pastes)
|
||||
pendingSelectionRef.current = new Set([
|
||||
...(pendingSelectionRef.current ?? []),
|
||||
...pastedBlocksArray.map((b) => b.id),
|
||||
])
|
||||
setPendingSelection(pastedBlocksArray.map((b) => b.id))
|
||||
|
||||
collaborativeBatchAddBlocks(
|
||||
pastedBlocksArray,
|
||||
@@ -894,7 +896,14 @@ const WorkflowContent = React.memo(() => {
|
||||
pasteData.subBlockValues
|
||||
)
|
||||
},
|
||||
[preparePasteData, blocks, addNotification, activeWorkflowId, collaborativeBatchAddBlocks]
|
||||
[
|
||||
preparePasteData,
|
||||
blocks,
|
||||
addNotification,
|
||||
activeWorkflowId,
|
||||
collaborativeBatchAddBlocks,
|
||||
setPendingSelection,
|
||||
]
|
||||
)
|
||||
|
||||
const handleContextPaste = useCallback(() => {
|
||||
@@ -2041,26 +2050,28 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
useEffect(() => {
|
||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||
const pendingSelection = pendingSelectionRef.current
|
||||
pendingSelectionRef.current = null
|
||||
if (pendingSelection && pendingSelection.length > 0) {
|
||||
const pendingSet = new Set(pendingSelection)
|
||||
clearPendingSelection()
|
||||
|
||||
// Apply pending selection and resolve parent-child conflicts
|
||||
const withSelection = derivedNodes.map((node) => ({
|
||||
...node,
|
||||
selected: pendingSet.has(node.id),
|
||||
}))
|
||||
setDisplayNodes(resolveParentChildSelectionConflicts(withSelection, blocks))
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve existing selection state
|
||||
setDisplayNodes((currentNodes) => {
|
||||
if (pendingSelection) {
|
||||
// Apply pending selection and resolve parent-child conflicts
|
||||
const withSelection = derivedNodes.map((node) => ({
|
||||
...node,
|
||||
selected: pendingSelection.has(node.id),
|
||||
}))
|
||||
return resolveParentChildSelectionConflicts(withSelection, blocks)
|
||||
}
|
||||
// Preserve existing selection state
|
||||
const selectedIds = new Set(currentNodes.filter((n) => n.selected).map((n) => n.id))
|
||||
return derivedNodes.map((node) => ({
|
||||
...node,
|
||||
selected: selectedIds.has(node.id),
|
||||
}))
|
||||
})
|
||||
}, [derivedNodes, blocks])
|
||||
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
|
||||
|
||||
/** Handles ActionBar remove-from-subflow events. */
|
||||
useEffect(() => {
|
||||
@@ -3010,23 +3021,6 @@ const WorkflowContent = React.memo(() => {
|
||||
usePanelEditorStore.getState().clearCurrentBlock()
|
||||
}, [])
|
||||
|
||||
/** Prevents native text selection when starting a shift-drag on the pane. */
|
||||
const handleCanvasMouseDown = useCallback((event: React.MouseEvent) => {
|
||||
if (!event.shiftKey) return
|
||||
|
||||
const target = event.target as HTMLElement | null
|
||||
if (!target) return
|
||||
|
||||
const isPaneTarget = Boolean(target.closest('.react-flow__pane, .react-flow__selectionpane'))
|
||||
if (!isPaneTarget) return
|
||||
|
||||
event.preventDefault()
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
selection.removeAllRanges()
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Handles node click to select the node in ReactFlow.
|
||||
* Parent-child conflict resolution happens automatically in onNodesChange.
|
||||
@@ -3226,9 +3220,10 @@ const WorkflowContent = React.memo(() => {
|
||||
onPointerMove={handleCanvasPointerMove}
|
||||
onPointerLeave={handleCanvasPointerLeave}
|
||||
elementsSelectable={true}
|
||||
selectionOnDrag={!isHandMode}
|
||||
selectionOnDrag={selectionProps.selectionOnDrag}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
panOnDrag={isHandMode ? [0, 1] : false}
|
||||
panOnDrag={selectionProps.panOnDrag}
|
||||
selectionKeyCode={selectionProps.selectionKeyCode}
|
||||
multiSelectionKeyCode={['Meta', 'Control', 'Shift']}
|
||||
nodesConnectable={effectivePermissions.canEdit}
|
||||
nodesDraggable={effectivePermissions.canEdit}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen
|
||||
import { SubflowNodeComponent } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
|
||||
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
|
||||
import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge'
|
||||
import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-node-utilities'
|
||||
import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
|
||||
import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/block'
|
||||
import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/subflow'
|
||||
import { getBlock } from '@/blocks'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import * as VisuallyHidden from '@radix-ui/react-visually-hidden'
|
||||
import { BookOpen, Layout, RepeatIcon, ScrollText, Search, SplitIcon } from 'lucide-react'
|
||||
@@ -87,7 +87,90 @@ type SearchItem = {
|
||||
config?: any
|
||||
}
|
||||
|
||||
export function SearchModal({
|
||||
interface SearchResultItemProps {
|
||||
item: SearchItem
|
||||
visualIndex: number
|
||||
isSelected: boolean
|
||||
onItemClick: (item: SearchItem) => void
|
||||
}
|
||||
|
||||
const SearchResultItem = memo(function SearchResultItem({
|
||||
item,
|
||||
visualIndex,
|
||||
isSelected,
|
||||
onItemClick,
|
||||
}: SearchResultItemProps) {
|
||||
const Icon = item.icon
|
||||
const showColoredIcon = item.type === 'block' || item.type === 'trigger' || item.type === 'tool'
|
||||
const isWorkflow = item.type === 'workflow'
|
||||
const isWorkspace = item.type === 'workspace'
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onItemClick(item)
|
||||
}, [onItemClick, item])
|
||||
|
||||
return (
|
||||
<button
|
||||
data-search-item-index={visualIndex}
|
||||
onClick={handleClick}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'group flex h-[28px] w-full items-center gap-[8px] rounded-[6px] bg-[var(--surface-4)]/60 px-[10px] text-left text-[15px] transition-all focus:outline-none',
|
||||
isSelected ? 'bg-[var(--border)] shadow-sm' : 'hover:bg-[var(--border)]'
|
||||
)}
|
||||
>
|
||||
{/* Icon - different rendering for workflows vs others */}
|
||||
{!isWorkspace && (
|
||||
<>
|
||||
{isWorkflow ? (
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
) : (
|
||||
Icon && (
|
||||
<div
|
||||
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
|
||||
style={{ background: showColoredIcon ? item.bgColor : 'transparent' }}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
'transition-transform duration-100 group-hover:scale-110',
|
||||
showColoredIcon
|
||||
? '!h-[10px] !w-[10px] text-white'
|
||||
: 'h-[14px] w-[14px] text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<span
|
||||
className={cn(
|
||||
'truncate font-medium',
|
||||
isSelected
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
{item.isCurrent && ' (current)'}
|
||||
</span>
|
||||
|
||||
{/* Shortcut */}
|
||||
{item.shortcut && (
|
||||
<span className='ml-auto flex-shrink-0 font-medium text-[13px] text-[var(--text-subtle)]'>
|
||||
{item.shortcut}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export const SearchModal = memo(function SearchModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
workflows = [],
|
||||
@@ -103,7 +186,7 @@ export function SearchModal({
|
||||
const { filterBlocks } = usePermissionConfig()
|
||||
|
||||
const blocks = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
if (!open || !isOnWorkflowPage) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
const filteredAllBlocks = filterBlocks(allBlocks)
|
||||
@@ -142,10 +225,10 @@ export function SearchModal({
|
||||
]
|
||||
|
||||
return [...regularBlocks, ...filterBlocks(specialBlocks)]
|
||||
}, [isOnWorkflowPage, filterBlocks])
|
||||
}, [open, isOnWorkflowPage, filterBlocks])
|
||||
|
||||
const triggers = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
if (!open || !isOnWorkflowPage) return []
|
||||
|
||||
const allTriggers = getTriggersForSidebar()
|
||||
const filteredTriggers = filterBlocks(allTriggers)
|
||||
@@ -174,10 +257,10 @@ export function SearchModal({
|
||||
config: block,
|
||||
})
|
||||
)
|
||||
}, [isOnWorkflowPage, filterBlocks])
|
||||
}, [open, isOnWorkflowPage, filterBlocks])
|
||||
|
||||
const tools = useMemo(() => {
|
||||
if (!isOnWorkflowPage) return []
|
||||
if (!open || !isOnWorkflowPage) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
const filteredAllBlocks = filterBlocks(allBlocks)
|
||||
@@ -193,7 +276,7 @@ export function SearchModal({
|
||||
type: block.type,
|
||||
})
|
||||
)
|
||||
}, [isOnWorkflowPage, filterBlocks])
|
||||
}, [open, isOnWorkflowPage, filterBlocks])
|
||||
|
||||
const pages = useMemo(
|
||||
(): PageItem[] => [
|
||||
@@ -221,6 +304,8 @@ export function SearchModal({
|
||||
)
|
||||
|
||||
const docs = useMemo((): DocItem[] => {
|
||||
if (!open) return []
|
||||
|
||||
const allBlocks = getAllBlocks()
|
||||
const docsItems: DocItem[] = []
|
||||
|
||||
@@ -237,7 +322,7 @@ export function SearchModal({
|
||||
})
|
||||
|
||||
return docsItems
|
||||
}, [])
|
||||
}, [open])
|
||||
|
||||
const allItems = useMemo((): SearchItem[] => {
|
||||
const items: SearchItem[] = []
|
||||
@@ -549,78 +634,16 @@ export function SearchModal({
|
||||
|
||||
{/* Section items */}
|
||||
<div className='space-y-[2px]'>
|
||||
{items.map((item, itemIndex) => {
|
||||
const Icon = item.icon
|
||||
{items.map((item) => {
|
||||
const visualIndex = displayedItemsInVisualOrder.indexOf(item)
|
||||
const isSelected = visualIndex === selectedIndex
|
||||
const showColoredIcon =
|
||||
item.type === 'block' || item.type === 'trigger' || item.type === 'tool'
|
||||
const isWorkflow = item.type === 'workflow'
|
||||
const isWorkspace = item.type === 'workspace'
|
||||
|
||||
return (
|
||||
<button
|
||||
<SearchResultItem
|
||||
key={`${item.type}-${item.id}`}
|
||||
data-search-item-index={visualIndex}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
className={cn(
|
||||
'group flex h-[28px] w-full items-center gap-[8px] rounded-[6px] bg-[var(--surface-4)]/60 px-[10px] text-left text-[15px] transition-all focus:outline-none',
|
||||
isSelected
|
||||
? 'bg-[var(--border)] shadow-sm'
|
||||
: 'hover:bg-[var(--border)]'
|
||||
)}
|
||||
>
|
||||
{/* Icon - different rendering for workflows vs others */}
|
||||
{!isWorkspace && (
|
||||
<>
|
||||
{isWorkflow ? (
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px]'
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
) : (
|
||||
Icon && (
|
||||
<div
|
||||
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
|
||||
style={{
|
||||
background: showColoredIcon ? item.bgColor : 'transparent',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className={cn(
|
||||
'transition-transform duration-100 group-hover:scale-110',
|
||||
showColoredIcon
|
||||
? '!h-[10px] !w-[10px] text-white'
|
||||
: 'h-[14px] w-[14px] text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<span
|
||||
className={cn(
|
||||
'truncate font-medium',
|
||||
isSelected
|
||||
? 'text-[var(--text-primary)]'
|
||||
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
{item.isCurrent && ' (current)'}
|
||||
</span>
|
||||
|
||||
{/* Shortcut */}
|
||||
{item.shortcut && (
|
||||
<span className='ml-auto flex-shrink-0 font-medium text-[13px] text-[var(--text-subtle)]'>
|
||||
{item.shortcut}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
item={item}
|
||||
visualIndex={visualIndex}
|
||||
isSelected={visualIndex === selectedIndex}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -639,4 +662,4 @@ export function SearchModal({
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1078,7 +1078,7 @@ export function AccessControl() {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
variant='destructive'
|
||||
onClick={() => {
|
||||
setShowUnsavedChanges(false)
|
||||
setShowConfigModal(false)
|
||||
|
||||
@@ -294,10 +294,9 @@ export function BYOK() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
variant='tertiary'
|
||||
onClick={handleSave}
|
||||
disabled={!apiKeyInput.trim() || upsertKey.isPending}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
{upsertKey.isPending ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
@@ -321,12 +320,7 @@ export function BYOK() {
|
||||
<Button variant='default' onClick={() => setDeleteConfirmProvider(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDelete}
|
||||
disabled={deleteKey.isPending}
|
||||
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
>
|
||||
<Button variant='destructive' onClick={handleDelete} disabled={deleteKey.isPending}>
|
||||
{deleteKey.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -334,7 +334,7 @@ export function Copilot() {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
variant='destructive'
|
||||
onClick={handleDeleteKey}
|
||||
disabled={deleteKeyMutation.isPending}
|
||||
>
|
||||
|
||||
@@ -831,7 +831,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='default' onClick={handleCancel}>
|
||||
<Button variant='destructive' onClick={handleCancel}>
|
||||
Discard Changes
|
||||
</Button>
|
||||
{hasConflicts || hasInvalidKeys ? (
|
||||
|
||||
@@ -117,7 +117,7 @@ export function TeamSeats({
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant='active' onClick={() => onOpenChange(false)} disabled={isLoading}>
|
||||
<Button variant='default' onClick={() => onOpenChange(false)} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ export function WorkflowItem({
|
||||
}: WorkflowItemProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const { selectedWorkflows } = useFolderStore()
|
||||
const { updateWorkflow, workflows } = useWorkflowRegistry()
|
||||
const selectedWorkflows = useFolderStore((state) => state.selectedWorkflows)
|
||||
const updateWorkflow = useWorkflowRegistry((state) => state.updateWorkflow)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
const isSelected = selectedWorkflows.has(workflow.id)
|
||||
|
||||
@@ -141,6 +141,7 @@ export function WorkflowItem({
|
||||
const workflowIds =
|
||||
finalIsSelected && finalSelection.size > 1 ? Array.from(finalSelection) : [workflow.id]
|
||||
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
const workflowNames = workflowIds
|
||||
.map((id) => workflows[id]?.name)
|
||||
.filter((name): name is string => !!name)
|
||||
@@ -151,7 +152,7 @@ export function WorkflowItem({
|
||||
}
|
||||
|
||||
setCanDeleteCaptured(canDeleteWorkflows(workflowIds))
|
||||
}, [workflow.id, workflows, canDeleteWorkflows])
|
||||
}, [workflow.id, canDeleteWorkflows])
|
||||
|
||||
/**
|
||||
* Handle right-click - ensure proper selection behavior and capture selection state
|
||||
|
||||
@@ -709,7 +709,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='default'
|
||||
variant='tertiary'
|
||||
disabled={isSaving || isSubmitting}
|
||||
onClick={handleSaveChanges}
|
||||
className='h-[32px] gap-[8px] px-[12px] font-medium'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
||||
import { useCreateWorkflow, useWorkflows } from '@/hooks/queries/workflows'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
@@ -15,19 +16,19 @@ interface UseWorkflowOperationsProps {
|
||||
|
||||
export function useWorkflowOperations({ workspaceId }: UseWorkflowOperationsProps) {
|
||||
const router = useRouter()
|
||||
const { workflows } = useWorkflowRegistry()
|
||||
const workflows = useWorkflowRegistry(useShallow((state) => state.workflows))
|
||||
const workflowsQuery = useWorkflows(workspaceId)
|
||||
const createWorkflowMutation = useCreateWorkflow()
|
||||
|
||||
/**
|
||||
* Filter and sort workflows for the current workspace
|
||||
*/
|
||||
const regularWorkflows = Object.values(workflows)
|
||||
.filter((workflow) => workflow.workspaceId === workspaceId)
|
||||
.sort((a, b) => {
|
||||
// Sort by creation date (newest first) for stable ordering
|
||||
return b.createdAt.getTime() - a.createdAt.getTime()
|
||||
})
|
||||
const regularWorkflows = useMemo(
|
||||
() =>
|
||||
Object.values(workflows)
|
||||
.filter((workflow) => workflow.workspaceId === workspaceId)
|
||||
.sort((a, b) => {
|
||||
return b.createdAt.getTime() - a.createdAt.getTime()
|
||||
}),
|
||||
[workflows, workspaceId]
|
||||
)
|
||||
|
||||
const handleCreateWorkflow = useCallback(async (): Promise<string | null> => {
|
||||
try {
|
||||
@@ -55,13 +56,11 @@ export function useWorkflowOperations({ workspaceId }: UseWorkflowOperationsProp
|
||||
}, [createWorkflowMutation, workspaceId, router])
|
||||
|
||||
return {
|
||||
// State
|
||||
workflows,
|
||||
regularWorkflows,
|
||||
workflowsLoading: workflowsQuery.isLoading,
|
||||
isCreatingWorkflow: createWorkflowMutation.isPending,
|
||||
|
||||
// Operations
|
||||
handleCreateWorkflow,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useWorkspaceManagement({
|
||||
}: UseWorkspaceManagementProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { switchToWorkspace } = useWorkflowRegistry()
|
||||
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
|
||||
|
||||
// Workspace management state
|
||||
const [workspaces, setWorkspaces] = useState<Workspace[]>([])
|
||||
@@ -95,10 +95,6 @@ export function useWorkspaceManagement({
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Fetch workspaces for the current user with full validation and URL handling
|
||||
* Uses refs for workspaceId and router to avoid unnecessary recreations
|
||||
*/
|
||||
const fetchWorkspaces = useCallback(async () => {
|
||||
setIsWorkspacesLoading(true)
|
||||
try {
|
||||
@@ -181,10 +177,6 @@ export function useWorkspaceManagement({
|
||||
[]
|
||||
)
|
||||
|
||||
/**
|
||||
* Switch to a different workspace
|
||||
* Uses refs for activeWorkspace and router to avoid unnecessary recreations
|
||||
*/
|
||||
const switchWorkspace = useCallback(
|
||||
async (workspace: Workspace) => {
|
||||
// If already on this workspace, return
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Database, HelpCircle, Layout, Plus, Search, Settings } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -60,7 +60,7 @@ export const SIDEBAR_SCROLL_EVENT = 'sidebar-scroll-to-item'
|
||||
*
|
||||
* @returns Sidebar with workflows panel
|
||||
*/
|
||||
export function Sidebar() {
|
||||
export const Sidebar = memo(function Sidebar() {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const workflowId = params.workflowId as string | undefined
|
||||
@@ -142,11 +142,9 @@ export function Sidebar() {
|
||||
window.removeEventListener(SIDEBAR_SCROLL_EVENT, handleScrollToItem as EventListener)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
isOpen: isSearchModalOpen,
|
||||
setOpen: setIsSearchModalOpen,
|
||||
open: openSearchModal,
|
||||
} = useSearchModalStore()
|
||||
const isSearchModalOpen = useSearchModalStore((state) => state.isOpen)
|
||||
const setIsSearchModalOpen = useSearchModalStore((state) => state.setOpen)
|
||||
const openSearchModal = useSearchModalStore((state) => state.open)
|
||||
|
||||
const {
|
||||
workspaces,
|
||||
@@ -176,7 +174,6 @@ export function Sidebar() {
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
/** Context menu state for navigation items */
|
||||
const [activeNavItemHref, setActiveNavItemHref] = useState<string | null>(null)
|
||||
const {
|
||||
isOpen: isNavContextMenuOpen,
|
||||
@@ -285,7 +282,6 @@ export function Sidebar() {
|
||||
const isLoading = workflowsLoading || sessionLoading
|
||||
const initialScrollDoneRef = useRef(false)
|
||||
|
||||
/** Scrolls to active workflow on initial page load only */
|
||||
useEffect(() => {
|
||||
if (!workflowId || workflowsLoading || initialScrollDoneRef.current) return
|
||||
initialScrollDoneRef.current = true
|
||||
@@ -296,7 +292,6 @@ export function Sidebar() {
|
||||
})
|
||||
}, [workflowId, workflowsLoading])
|
||||
|
||||
/** Forces sidebar to minimum width and ensures it's expanded when not on a workflow page */
|
||||
useEffect(() => {
|
||||
if (!isOnWorkflowPage) {
|
||||
if (isCollapsed) {
|
||||
@@ -306,7 +301,6 @@ export function Sidebar() {
|
||||
}
|
||||
}, [isOnWorkflowPage, isCollapsed, setIsCollapsed, setSidebarWidth])
|
||||
|
||||
/** Creates a workflow and scrolls to it */
|
||||
const handleCreateWorkflow = useCallback(async () => {
|
||||
const workflowId = await createWorkflow()
|
||||
if (workflowId) {
|
||||
@@ -316,7 +310,6 @@ export function Sidebar() {
|
||||
}
|
||||
}, [createWorkflow])
|
||||
|
||||
/** Creates a folder and scrolls to it */
|
||||
const handleCreateFolder = useCallback(async () => {
|
||||
const folderId = await createFolder()
|
||||
if (folderId) {
|
||||
@@ -324,12 +317,10 @@ export function Sidebar() {
|
||||
}
|
||||
}, [createFolder])
|
||||
|
||||
/** Triggers file input for workflow import */
|
||||
const handleImportWorkflow = useCallback(() => {
|
||||
fileInputRef.current?.click()
|
||||
}, [])
|
||||
|
||||
/** Handles workspace switch from popover menu */
|
||||
const handleWorkspaceSwitch = useCallback(
|
||||
async (workspace: { id: string; name: string; ownerId: string; role?: string }) => {
|
||||
if (workspace.id === workspaceId) {
|
||||
@@ -342,12 +333,10 @@ export function Sidebar() {
|
||||
[workspaceId, switchWorkspace]
|
||||
)
|
||||
|
||||
/** Toggles sidebar collapse state */
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
setIsCollapsed(!isCollapsed)
|
||||
}, [isCollapsed, setIsCollapsed])
|
||||
|
||||
/** Reverts to active workflow selection when clicking sidebar background */
|
||||
const handleSidebarClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => {
|
||||
const target = e.target as HTMLElement
|
||||
@@ -360,7 +349,6 @@ export function Sidebar() {
|
||||
[workflowId]
|
||||
)
|
||||
|
||||
/** Renames a workspace */
|
||||
const handleRenameWorkspace = useCallback(
|
||||
async (workspaceIdToRename: string, newName: string) => {
|
||||
await updateWorkspaceName(workspaceIdToRename, newName)
|
||||
@@ -368,7 +356,6 @@ export function Sidebar() {
|
||||
[updateWorkspaceName]
|
||||
)
|
||||
|
||||
/** Deletes a workspace */
|
||||
const handleDeleteWorkspace = useCallback(
|
||||
async (workspaceIdToDelete: string) => {
|
||||
const workspaceToDelete = workspaces.find((w) => w.id === workspaceIdToDelete)
|
||||
@@ -379,7 +366,6 @@ export function Sidebar() {
|
||||
[workspaces, confirmDeleteWorkspace]
|
||||
)
|
||||
|
||||
/** Leaves a workspace */
|
||||
const handleLeaveWorkspaceWrapper = useCallback(
|
||||
async (workspaceIdToLeave: string) => {
|
||||
const workspaceToLeave = workspaces.find((w) => w.id === workspaceIdToLeave)
|
||||
@@ -390,7 +376,6 @@ export function Sidebar() {
|
||||
[workspaces, handleLeaveWorkspace]
|
||||
)
|
||||
|
||||
/** Duplicates a workspace */
|
||||
const handleDuplicateWorkspace = useCallback(
|
||||
async (_workspaceIdToDuplicate: string, workspaceName: string) => {
|
||||
await duplicateWorkspace(workspaceName)
|
||||
@@ -398,7 +383,6 @@ export function Sidebar() {
|
||||
[duplicateWorkspace]
|
||||
)
|
||||
|
||||
/** Exports a workspace */
|
||||
const handleExportWorkspace = useCallback(
|
||||
async (workspaceIdToExport: string, workspaceName: string) => {
|
||||
await exportWorkspace(workspaceIdToExport, workspaceName)
|
||||
@@ -406,12 +390,10 @@ export function Sidebar() {
|
||||
[exportWorkspace]
|
||||
)
|
||||
|
||||
/** Triggers file input for workspace import */
|
||||
const handleImportWorkspace = useCallback(() => {
|
||||
workspaceFileInputRef.current?.click()
|
||||
}, [])
|
||||
|
||||
/** Handles workspace import file selection */
|
||||
const handleWorkspaceFileChange = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files
|
||||
@@ -427,7 +409,6 @@ export function Sidebar() {
|
||||
[importWorkspace]
|
||||
)
|
||||
|
||||
/** Resolves workspace ID from params or URL path */
|
||||
const resolveWorkspaceIdFromPath = useCallback((): string | undefined => {
|
||||
if (workspaceId) return workspaceId
|
||||
if (typeof window === 'undefined') return undefined
|
||||
@@ -439,7 +420,6 @@ export function Sidebar() {
|
||||
return parts[idx + 1]
|
||||
}, [workspaceId])
|
||||
|
||||
/** Registers global sidebar commands with the central commands registry */
|
||||
useRegisterGlobalCommands(() =>
|
||||
createCommands([
|
||||
{
|
||||
@@ -772,4 +752,4 @@ export function Sidebar() {
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { getNextWorkflowColor } from '@/lib/workflows/colors'
|
||||
@@ -27,9 +27,21 @@ interface UseDuplicateWorkflowProps {
|
||||
*/
|
||||
export function useDuplicateWorkflow({ workspaceId, onSuccess }: UseDuplicateWorkflowProps) {
|
||||
const router = useRouter()
|
||||
const { workflows } = useWorkflowRegistry()
|
||||
const duplicateMutation = useDuplicateWorkflowMutation()
|
||||
|
||||
const workspaceIdRef = useRef(workspaceId)
|
||||
workspaceIdRef.current = workspaceId
|
||||
|
||||
const onSuccessRef = useRef(onSuccess)
|
||||
onSuccessRef.current = onSuccess
|
||||
|
||||
/**
|
||||
* Store a ref to the mutation to access isPending without causing callback recreation.
|
||||
* The mutateAsync function from React Query is already stable.
|
||||
*/
|
||||
const mutationRef = useRef(duplicateMutation)
|
||||
mutationRef.current = duplicateMutation
|
||||
|
||||
/**
|
||||
* Duplicate the workflow(s)
|
||||
* @param workflowIds - The workflow ID(s) to duplicate
|
||||
@@ -40,7 +52,7 @@ export function useDuplicateWorkflow({ workspaceId, onSuccess }: UseDuplicateWor
|
||||
return
|
||||
}
|
||||
|
||||
if (duplicateMutation.isPending) {
|
||||
if (mutationRef.current.isPending) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,6 +61,8 @@ export function useDuplicateWorkflow({ workspaceId, onSuccess }: UseDuplicateWor
|
||||
const duplicatedIds: string[] = []
|
||||
|
||||
try {
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
|
||||
for (const sourceId of workflowIdsToDuplicate) {
|
||||
const sourceWorkflow = workflows[sourceId]
|
||||
if (!sourceWorkflow) {
|
||||
@@ -56,8 +70,8 @@ export function useDuplicateWorkflow({ workspaceId, onSuccess }: UseDuplicateWor
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await duplicateMutation.mutateAsync({
|
||||
workspaceId,
|
||||
const result = await mutationRef.current.mutateAsync({
|
||||
workspaceId: workspaceIdRef.current,
|
||||
sourceId,
|
||||
name: `${sourceWorkflow.name} (Copy)`,
|
||||
description: sourceWorkflow.description,
|
||||
@@ -77,16 +91,16 @@ export function useDuplicateWorkflow({ workspaceId, onSuccess }: UseDuplicateWor
|
||||
})
|
||||
|
||||
if (duplicatedIds.length === 1) {
|
||||
router.push(`/workspace/${workspaceId}/w/${duplicatedIds[0]}`)
|
||||
router.push(`/workspace/${workspaceIdRef.current}/w/${duplicatedIds[0]}`)
|
||||
}
|
||||
|
||||
onSuccess?.()
|
||||
onSuccessRef.current?.()
|
||||
} catch (error) {
|
||||
logger.error('Error duplicating workflow(s):', { error })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[duplicateMutation, workflows, workspaceId, router, onSuccess]
|
||||
[router]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
downloadFile,
|
||||
@@ -23,9 +23,11 @@ interface UseExportWorkflowProps {
|
||||
* Hook for managing workflow export to JSON or ZIP.
|
||||
*/
|
||||
export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) {
|
||||
const { workflows } = useWorkflowRegistry()
|
||||
const [isExporting, setIsExporting] = useState(false)
|
||||
|
||||
const onSuccessRef = useRef(onSuccess)
|
||||
onSuccessRef.current = onSuccess
|
||||
|
||||
/**
|
||||
* Export the workflow(s) to JSON or ZIP
|
||||
* - Single workflow: exports as JSON file
|
||||
@@ -50,6 +52,7 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) {
|
||||
count: workflowIdsToExport.length,
|
||||
})
|
||||
|
||||
const { workflows } = useWorkflowRegistry.getState()
|
||||
const exportedWorkflows = []
|
||||
|
||||
for (const workflowId of workflowIdsToExport) {
|
||||
@@ -96,7 +99,7 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) {
|
||||
format: exportedWorkflows.length === 1 ? 'JSON' : 'ZIP',
|
||||
})
|
||||
|
||||
onSuccess?.()
|
||||
onSuccessRef.current?.()
|
||||
} catch (error) {
|
||||
logger.error('Error exporting workflow(s):', { error })
|
||||
throw error
|
||||
@@ -104,7 +107,7 @@ export function useExportWorkflow({ onSuccess }: UseExportWorkflowProps = {}) {
|
||||
setIsExporting(false)
|
||||
}
|
||||
},
|
||||
[isExporting, workflows, onSuccess]
|
||||
[isExporting]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
@@ -739,38 +740,60 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
eventHandlers.current.operationFailed = handler
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SocketContext.Provider
|
||||
value={{
|
||||
socket,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
currentWorkflowId,
|
||||
currentSocketId,
|
||||
presenceUsers,
|
||||
joinWorkflow,
|
||||
leaveWorkflow,
|
||||
emitWorkflowOperation,
|
||||
emitSubblockUpdate,
|
||||
emitVariableUpdate,
|
||||
|
||||
emitCursorUpdate,
|
||||
emitSelectionUpdate,
|
||||
onWorkflowOperation,
|
||||
onSubblockUpdate,
|
||||
onVariableUpdate,
|
||||
|
||||
onCursorUpdate,
|
||||
onSelectionUpdate,
|
||||
onUserJoined,
|
||||
onUserLeft,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SocketContext.Provider>
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
socket,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
currentWorkflowId,
|
||||
currentSocketId,
|
||||
presenceUsers,
|
||||
joinWorkflow,
|
||||
leaveWorkflow,
|
||||
emitWorkflowOperation,
|
||||
emitSubblockUpdate,
|
||||
emitVariableUpdate,
|
||||
emitCursorUpdate,
|
||||
emitSelectionUpdate,
|
||||
onWorkflowOperation,
|
||||
onSubblockUpdate,
|
||||
onVariableUpdate,
|
||||
onCursorUpdate,
|
||||
onSelectionUpdate,
|
||||
onUserJoined,
|
||||
onUserLeft,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
}),
|
||||
[
|
||||
socket,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
currentWorkflowId,
|
||||
currentSocketId,
|
||||
presenceUsers,
|
||||
joinWorkflow,
|
||||
leaveWorkflow,
|
||||
emitWorkflowOperation,
|
||||
emitSubblockUpdate,
|
||||
emitVariableUpdate,
|
||||
emitCursorUpdate,
|
||||
emitSelectionUpdate,
|
||||
onWorkflowOperation,
|
||||
onSubblockUpdate,
|
||||
onVariableUpdate,
|
||||
onCursorUpdate,
|
||||
onSelectionUpdate,
|
||||
onUserJoined,
|
||||
onUserLeft,
|
||||
onWorkflowDeleted,
|
||||
onWorkflowReverted,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
]
|
||||
)
|
||||
|
||||
return <SocketContext.Provider value={contextValue}>{children}</SocketContext.Provider>
|
||||
}
|
||||
|
||||
@@ -84,6 +84,44 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
{ label: 'Create project', id: 'github_create_project' },
|
||||
{ label: 'Update project', id: 'github_update_project' },
|
||||
{ label: 'Delete project', id: 'github_delete_project' },
|
||||
// Search Operations
|
||||
{ label: 'Search code', id: 'github_search_code' },
|
||||
{ label: 'Search commits', id: 'github_search_commits' },
|
||||
{ label: 'Search issues', id: 'github_search_issues' },
|
||||
{ label: 'Search repositories', id: 'github_search_repos' },
|
||||
{ label: 'Search users', id: 'github_search_users' },
|
||||
// Commit Operations
|
||||
{ label: 'List commits', id: 'github_list_commits' },
|
||||
{ label: 'Get commit', id: 'github_get_commit' },
|
||||
{ label: 'Compare commits', id: 'github_compare_commits' },
|
||||
// Gist Operations
|
||||
{ label: 'Create gist', id: 'github_create_gist' },
|
||||
{ label: 'Get gist', id: 'github_get_gist' },
|
||||
{ label: 'List gists', id: 'github_list_gists' },
|
||||
{ label: 'Update gist', id: 'github_update_gist' },
|
||||
{ label: 'Delete gist', id: 'github_delete_gist' },
|
||||
{ label: 'Fork gist', id: 'github_fork_gist' },
|
||||
{ label: 'Star gist', id: 'github_star_gist' },
|
||||
{ label: 'Unstar gist', id: 'github_unstar_gist' },
|
||||
// Fork Operations
|
||||
{ label: 'Fork repository', id: 'github_fork_repo' },
|
||||
{ label: 'List forks', id: 'github_list_forks' },
|
||||
// Milestone Operations
|
||||
{ label: 'Create milestone', id: 'github_create_milestone' },
|
||||
{ label: 'Get milestone', id: 'github_get_milestone' },
|
||||
{ label: 'List milestones', id: 'github_list_milestones' },
|
||||
{ label: 'Update milestone', id: 'github_update_milestone' },
|
||||
{ label: 'Delete milestone', id: 'github_delete_milestone' },
|
||||
// Reaction Operations
|
||||
{ label: 'Add issue reaction', id: 'github_create_issue_reaction' },
|
||||
{ label: 'Remove issue reaction', id: 'github_delete_issue_reaction' },
|
||||
{ label: 'Add comment reaction', id: 'github_create_comment_reaction' },
|
||||
{ label: 'Remove comment reaction', id: 'github_delete_comment_reaction' },
|
||||
// Star Operations
|
||||
{ label: 'Star repository', id: 'github_star_repo' },
|
||||
{ label: 'Unstar repository', id: 'github_unstar_repo' },
|
||||
{ label: 'Check if starred', id: 'github_check_star' },
|
||||
{ label: 'List stargazers', id: 'github_list_stargazers' },
|
||||
],
|
||||
value: () => 'github_pr',
|
||||
},
|
||||
@@ -998,6 +1036,440 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_delete_project' },
|
||||
},
|
||||
// Search operations parameters
|
||||
{
|
||||
id: 'q',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., react language:typescript',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'github_search_code',
|
||||
'github_search_commits',
|
||||
'github_search_issues',
|
||||
'github_search_repos',
|
||||
'github_search_users',
|
||||
],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a GitHub search query based on the user's description.
|
||||
GitHub search supports these qualifiers:
|
||||
- For repos: language:python, stars:>1000, forks:>100, topic:react, user:owner, org:name, created:>2023-01-01
|
||||
- For code: repo:owner/name, path:src, extension:ts, language:javascript
|
||||
- For issues/PRs: is:issue, is:pr, is:open, is:closed, label:bug, author:user, assignee:user
|
||||
- For commits: repo:owner/name, author:user, committer:user, author-date:>2023-01-01
|
||||
- For users: type:user, type:org, followers:>100, repos:>10, location:city
|
||||
|
||||
Examples:
|
||||
- "Python repos with more than 1000 stars" -> language:python stars:>1000
|
||||
- "Open bugs in facebook/react" -> repo:facebook/react is:issue is:open label:bug
|
||||
- "TypeScript files in src folder" -> language:typescript path:src
|
||||
|
||||
Return ONLY the search query - no explanations.`,
|
||||
placeholder: 'Describe what you want to search for...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sort',
|
||||
title: 'Sort By',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Best match', id: '' },
|
||||
{ label: 'Stars', id: 'stars' },
|
||||
{ label: 'Forks', id: 'forks' },
|
||||
{ label: 'Updated', id: 'updated' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_search_repos' },
|
||||
},
|
||||
{
|
||||
id: 'order',
|
||||
title: 'Order',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Descending', id: 'desc' },
|
||||
{ label: 'Ascending', id: 'asc' },
|
||||
],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'github_search_code',
|
||||
'github_search_commits',
|
||||
'github_search_issues',
|
||||
'github_search_repos',
|
||||
'github_search_users',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Commit operations parameters
|
||||
{
|
||||
id: 'sha',
|
||||
title: 'SHA or Branch',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., main or abc123',
|
||||
condition: { field: 'operation', value: 'github_list_commits' },
|
||||
},
|
||||
{
|
||||
id: 'author',
|
||||
title: 'Author Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'GitHub username or email',
|
||||
condition: { field: 'operation', value: 'github_list_commits' },
|
||||
},
|
||||
{
|
||||
id: 'since',
|
||||
title: 'Since Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601: 2024-01-01T00:00:00Z',
|
||||
condition: { field: 'operation', value: ['github_list_commits', 'github_list_gists'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "last week" -> Calculate 7 days ago at 00:00:00Z
|
||||
- "yesterday" -> Calculate yesterday's date at 00:00:00Z
|
||||
- "beginning of this month" -> First day of current month at 00:00:00Z
|
||||
- "30 days ago" -> Calculate 30 days before current time
|
||||
- "January 1st 2024" -> 2024-01-01T00:00:00Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the start date (e.g., "last week", "beginning of month")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'until',
|
||||
title: 'Until Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601: 2024-12-31T23:59:59Z',
|
||||
condition: { field: 'operation', value: 'github_list_commits' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "now" -> Current timestamp
|
||||
- "end of today" -> Today's date at 23:59:59Z
|
||||
- "end of last week" -> Calculate end of last week
|
||||
- "yesterday" -> Yesterday's date at 23:59:59Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the end date (e.g., "now", "end of yesterday")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ref',
|
||||
title: 'Commit Reference',
|
||||
type: 'short-input',
|
||||
placeholder: 'SHA, branch, or tag',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_get_commit' },
|
||||
},
|
||||
{
|
||||
id: 'base',
|
||||
title: 'Base Reference',
|
||||
type: 'short-input',
|
||||
placeholder: 'Base branch/tag/SHA',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_compare_commits' },
|
||||
},
|
||||
{
|
||||
id: 'head',
|
||||
title: 'Head Reference',
|
||||
type: 'short-input',
|
||||
placeholder: 'Head branch/tag/SHA',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_compare_commits' },
|
||||
},
|
||||
// Gist operations parameters
|
||||
{
|
||||
id: 'gist_id',
|
||||
title: 'Gist ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., aa5a315d61ae9438b18d',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'github_get_gist',
|
||||
'github_update_gist',
|
||||
'github_delete_gist',
|
||||
'github_fork_gist',
|
||||
'github_star_gist',
|
||||
'github_unstar_gist',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'Description',
|
||||
type: 'short-input',
|
||||
placeholder: 'Gist description',
|
||||
condition: { field: 'operation', value: ['github_create_gist', 'github_update_gist'] },
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: 'Files (JSON)',
|
||||
type: 'long-input',
|
||||
placeholder: '{"file.txt": {"content": "Hello"}}',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_create_gist' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON object for GitHub Gist files based on the user's description.
|
||||
The format is: {"filename.ext": {"content": "file contents"}}
|
||||
|
||||
Examples:
|
||||
- "A Python hello world file" -> {"hello.py": {"content": "print('Hello, World!')"}}
|
||||
- "A README with project title" -> {"README.md": {"content": "# My Project\\n\\nDescription here"}}
|
||||
- "JavaScript function to add numbers" -> {"add.js": {"content": "function add(a, b) {\\n return a + b;\\n}"}}
|
||||
- "Two files: index.html and style.css" -> {"index.html": {"content": "<!DOCTYPE html>..."}, "style.css": {"content": "body { margin: 0; }"}}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown formatting.`,
|
||||
placeholder: 'Describe the files you want to create...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
title: 'Files (JSON)',
|
||||
type: 'long-input',
|
||||
placeholder: '{"file.txt": {"content": "Updated"}}',
|
||||
condition: { field: 'operation', value: 'github_update_gist' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON object for updating GitHub Gist files based on the user's description.
|
||||
The format is: {"filename.ext": {"content": "new contents"}}
|
||||
To delete a file, set its value to null: {"old-file.txt": null}
|
||||
To rename a file, set the new filename: {"old-name.txt": {"filename": "new-name.txt", "content": "..."}}
|
||||
|
||||
Examples:
|
||||
- "Update hello.py to print goodbye" -> {"hello.py": {"content": "print('Goodbye!')"}}
|
||||
- "Delete the old readme" -> {"README.md": null}
|
||||
- "Rename script.js to main.js" -> {"script.js": {"filename": "main.js"}}
|
||||
|
||||
Return ONLY valid JSON - no explanations, no markdown formatting.`,
|
||||
placeholder: 'Describe the file changes...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'gist_public',
|
||||
title: 'Public',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Secret', id: 'false' },
|
||||
{ label: 'Public', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_create_gist' },
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
type: 'short-input',
|
||||
placeholder: 'GitHub username (optional)',
|
||||
condition: { field: 'operation', value: 'github_list_gists' },
|
||||
},
|
||||
// Fork operations parameters
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organization',
|
||||
type: 'short-input',
|
||||
placeholder: 'Fork to org (optional)',
|
||||
condition: { field: 'operation', value: 'github_fork_repo' },
|
||||
},
|
||||
{
|
||||
id: 'fork_name',
|
||||
title: 'Fork Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Custom name (optional)',
|
||||
condition: { field: 'operation', value: 'github_fork_repo' },
|
||||
},
|
||||
{
|
||||
id: 'default_branch_only',
|
||||
title: 'Default Branch Only',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No', id: 'false' },
|
||||
{ label: 'Yes', id: 'true' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_fork_repo' },
|
||||
},
|
||||
{
|
||||
id: 'fork_sort',
|
||||
title: 'Sort By',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Newest', id: 'newest' },
|
||||
{ label: 'Oldest', id: 'oldest' },
|
||||
{ label: 'Stargazers', id: 'stargazers' },
|
||||
{ label: 'Watchers', id: 'watchers' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_list_forks' },
|
||||
},
|
||||
// Milestone operations parameters
|
||||
{
|
||||
id: 'milestone_title',
|
||||
title: 'Milestone Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., v1.0 Release',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'github_create_milestone' },
|
||||
},
|
||||
{
|
||||
id: 'milestone_title',
|
||||
title: 'New Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Updated title (optional)',
|
||||
condition: { field: 'operation', value: 'github_update_milestone' },
|
||||
},
|
||||
{
|
||||
id: 'milestone_description',
|
||||
title: 'Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'Milestone description',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_create_milestone', 'github_update_milestone'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'due_on',
|
||||
title: 'Due Date',
|
||||
type: 'short-input',
|
||||
placeholder: 'ISO 8601: 2024-12-31T23:59:59Z',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_create_milestone', 'github_update_milestone'],
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp for a milestone due date based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "end of this month" -> Last day of current month at 23:59:59Z
|
||||
- "next Friday" -> Calculate next Friday's date at 23:59:59Z
|
||||
- "in 2 weeks" -> Calculate 14 days from now at 23:59:59Z
|
||||
- "December 31st" -> 2024-12-31T23:59:59Z (current year)
|
||||
- "Q1 2025" -> 2025-03-31T23:59:59Z (end of Q1)
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the due date (e.g., "end of month", "next Friday")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'milestone_number',
|
||||
title: 'Milestone Number',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 1',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_get_milestone', 'github_update_milestone', 'github_delete_milestone'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'milestone_state',
|
||||
title: 'State Filter',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Open', id: 'open' },
|
||||
{ label: 'Closed', id: 'closed' },
|
||||
{ label: 'All', id: 'all' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_list_milestones' },
|
||||
},
|
||||
{
|
||||
id: 'milestone_sort',
|
||||
title: 'Sort By',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Due Date', id: 'due_on' },
|
||||
{ label: 'Completeness', id: 'completeness' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'github_list_milestones' },
|
||||
},
|
||||
// Reaction operations parameters
|
||||
{
|
||||
id: 'reaction_content',
|
||||
title: 'Reaction',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: '👍 +1', id: '+1' },
|
||||
{ label: '👎 -1', id: '-1' },
|
||||
{ label: '😄 Laugh', id: 'laugh' },
|
||||
{ label: '😕 Confused', id: 'confused' },
|
||||
{ label: '❤️ Heart', id: 'heart' },
|
||||
{ label: '🎉 Hooray', id: 'hooray' },
|
||||
{ label: '🚀 Rocket', id: 'rocket' },
|
||||
{ label: '👀 Eyes', id: 'eyes' },
|
||||
],
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_create_issue_reaction', 'github_create_comment_reaction'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'issue_number',
|
||||
title: 'Issue Number',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 123',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_create_issue_reaction', 'github_delete_issue_reaction'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'reaction_id',
|
||||
title: 'Reaction ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 12345678',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_delete_issue_reaction', 'github_delete_comment_reaction'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'comment_id',
|
||||
title: 'Comment ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 987654321',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['github_create_comment_reaction', 'github_delete_comment_reaction'],
|
||||
},
|
||||
},
|
||||
// Star operations parameters - owner/repo already covered by existing subBlocks
|
||||
{
|
||||
id: 'per_page',
|
||||
title: 'Results Per Page',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g., 30 (default: 30, max: 100)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'github_search_code',
|
||||
'github_search_commits',
|
||||
'github_search_issues',
|
||||
'github_search_repos',
|
||||
'github_search_users',
|
||||
'github_list_commits',
|
||||
'github_list_gists',
|
||||
'github_list_forks',
|
||||
'github_list_milestones',
|
||||
'github_list_stargazers',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'GitHub Token',
|
||||
@@ -1118,6 +1590,44 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
'github_create_project',
|
||||
'github_update_project',
|
||||
'github_delete_project',
|
||||
// Search tools
|
||||
'github_search_code',
|
||||
'github_search_commits',
|
||||
'github_search_issues',
|
||||
'github_search_repos',
|
||||
'github_search_users',
|
||||
// Commit tools
|
||||
'github_list_commits',
|
||||
'github_get_commit',
|
||||
'github_compare_commits',
|
||||
// Gist tools
|
||||
'github_create_gist',
|
||||
'github_get_gist',
|
||||
'github_list_gists',
|
||||
'github_update_gist',
|
||||
'github_delete_gist',
|
||||
'github_fork_gist',
|
||||
'github_star_gist',
|
||||
'github_unstar_gist',
|
||||
// Fork tools
|
||||
'github_fork_repo',
|
||||
'github_list_forks',
|
||||
// Milestone tools
|
||||
'github_create_milestone',
|
||||
'github_get_milestone',
|
||||
'github_list_milestones',
|
||||
'github_update_milestone',
|
||||
'github_delete_milestone',
|
||||
// Reaction tools
|
||||
'github_create_issue_reaction',
|
||||
'github_delete_issue_reaction',
|
||||
'github_create_comment_reaction',
|
||||
'github_delete_comment_reaction',
|
||||
// Star tools
|
||||
'github_star_repo',
|
||||
'github_unstar_repo',
|
||||
'github_check_star',
|
||||
'github_list_stargazers',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -1234,6 +1744,75 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
return 'github_update_project'
|
||||
case 'github_delete_project':
|
||||
return 'github_delete_project'
|
||||
// Search operations
|
||||
case 'github_search_code':
|
||||
return 'github_search_code'
|
||||
case 'github_search_commits':
|
||||
return 'github_search_commits'
|
||||
case 'github_search_issues':
|
||||
return 'github_search_issues'
|
||||
case 'github_search_repos':
|
||||
return 'github_search_repos'
|
||||
case 'github_search_users':
|
||||
return 'github_search_users'
|
||||
// Commit operations
|
||||
case 'github_list_commits':
|
||||
return 'github_list_commits'
|
||||
case 'github_get_commit':
|
||||
return 'github_get_commit'
|
||||
case 'github_compare_commits':
|
||||
return 'github_compare_commits'
|
||||
// Gist operations
|
||||
case 'github_create_gist':
|
||||
return 'github_create_gist'
|
||||
case 'github_get_gist':
|
||||
return 'github_get_gist'
|
||||
case 'github_list_gists':
|
||||
return 'github_list_gists'
|
||||
case 'github_update_gist':
|
||||
return 'github_update_gist'
|
||||
case 'github_delete_gist':
|
||||
return 'github_delete_gist'
|
||||
case 'github_fork_gist':
|
||||
return 'github_fork_gist'
|
||||
case 'github_star_gist':
|
||||
return 'github_star_gist'
|
||||
case 'github_unstar_gist':
|
||||
return 'github_unstar_gist'
|
||||
// Fork operations
|
||||
case 'github_fork_repo':
|
||||
return 'github_fork_repo'
|
||||
case 'github_list_forks':
|
||||
return 'github_list_forks'
|
||||
// Milestone operations
|
||||
case 'github_create_milestone':
|
||||
return 'github_create_milestone'
|
||||
case 'github_get_milestone':
|
||||
return 'github_get_milestone'
|
||||
case 'github_list_milestones':
|
||||
return 'github_list_milestones'
|
||||
case 'github_update_milestone':
|
||||
return 'github_update_milestone'
|
||||
case 'github_delete_milestone':
|
||||
return 'github_delete_milestone'
|
||||
// Reaction operations
|
||||
case 'github_create_issue_reaction':
|
||||
return 'github_create_issue_reaction'
|
||||
case 'github_delete_issue_reaction':
|
||||
return 'github_delete_issue_reaction'
|
||||
case 'github_create_comment_reaction':
|
||||
return 'github_create_comment_reaction'
|
||||
case 'github_delete_comment_reaction':
|
||||
return 'github_delete_comment_reaction'
|
||||
// Star operations
|
||||
case 'github_star_repo':
|
||||
return 'github_star_repo'
|
||||
case 'github_unstar_repo':
|
||||
return 'github_unstar_repo'
|
||||
case 'github_check_star':
|
||||
return 'github_check_star'
|
||||
case 'github_list_stargazers':
|
||||
return 'github_list_stargazers'
|
||||
default:
|
||||
return 'github_repo_info'
|
||||
}
|
||||
@@ -1297,6 +1876,38 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
|
||||
project_number: { type: 'number', description: 'Project number' },
|
||||
project_id: { type: 'string', description: 'Project node ID' },
|
||||
project_public: { type: 'boolean', description: 'Project public status' },
|
||||
// Search parameters
|
||||
q: { type: 'string', description: 'Search query with qualifiers' },
|
||||
sort: { type: 'string', description: 'Sort field' },
|
||||
order: { type: 'string', description: 'Sort order (asc or desc)' },
|
||||
// Commit parameters
|
||||
author: { type: 'string', description: 'Author filter' },
|
||||
committer: { type: 'string', description: 'Committer filter' },
|
||||
since: { type: 'string', description: 'Date filter (since)' },
|
||||
until: { type: 'string', description: 'Date filter (until)' },
|
||||
// Gist parameters
|
||||
gist_id: { type: 'string', description: 'Gist ID' },
|
||||
description: { type: 'string', description: 'Description' },
|
||||
files: { type: 'string', description: 'Files JSON object' },
|
||||
gist_public: { type: 'boolean', description: 'Public gist status' },
|
||||
username: { type: 'string', description: 'GitHub username' },
|
||||
// Fork parameters
|
||||
organization: { type: 'string', description: 'Target organization for fork' },
|
||||
fork_name: { type: 'string', description: 'Custom name for fork' },
|
||||
default_branch_only: { type: 'boolean', description: 'Fork only default branch' },
|
||||
fork_sort: { type: 'string', description: 'Fork list sort field' },
|
||||
// Milestone parameters
|
||||
milestone_title: { type: 'string', description: 'Milestone title' },
|
||||
milestone_description: { type: 'string', description: 'Milestone description' },
|
||||
due_on: { type: 'string', description: 'Milestone due date' },
|
||||
milestone_number: { type: 'number', description: 'Milestone number' },
|
||||
milestone_state: { type: 'string', description: 'Milestone state filter' },
|
||||
milestone_sort: { type: 'string', description: 'Milestone sort field' },
|
||||
// Reaction parameters
|
||||
reaction_content: { type: 'string', description: 'Reaction type' },
|
||||
reaction_id: { type: 'number', description: 'Reaction ID' },
|
||||
// Pagination parameters
|
||||
page: { type: 'number', description: 'Page number for pagination' },
|
||||
},
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Response content' },
|
||||
|
||||
@@ -25,6 +25,11 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
{ label: 'Create Event', id: 'create' },
|
||||
{ label: 'List Events', id: 'list' },
|
||||
{ label: 'Get Event', id: 'get' },
|
||||
{ label: 'Update Event', id: 'update' },
|
||||
{ label: 'Delete Event', id: 'delete' },
|
||||
{ label: 'Move Event', id: 'move' },
|
||||
{ label: 'Get Recurring Instances', id: 'instances' },
|
||||
{ label: 'List Calendars', id: 'list_calendars' },
|
||||
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
|
||||
{ label: 'Invite Attendees', id: 'invite' },
|
||||
],
|
||||
@@ -39,7 +44,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select Google Calendar account',
|
||||
},
|
||||
// Calendar selector (basic mode)
|
||||
// Calendar selector (basic mode) - not needed for list_calendars
|
||||
{
|
||||
id: 'calendarId',
|
||||
title: 'Calendar',
|
||||
@@ -50,8 +55,9 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
placeholder: 'Select calendar',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: 'list_calendars', not: true },
|
||||
},
|
||||
// Manual calendar ID input (advanced mode)
|
||||
// Manual calendar ID input (advanced mode) - not needed for list_calendars
|
||||
{
|
||||
id: 'manualCalendarId',
|
||||
title: 'Calendar ID',
|
||||
@@ -59,6 +65,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
canonicalParamId: 'calendarId',
|
||||
placeholder: 'Enter calendar ID (e.g., primary or calendar@gmail.com)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list_calendars', not: true },
|
||||
},
|
||||
|
||||
// Create Event Fields
|
||||
@@ -204,10 +211,179 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
title: 'Event ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Event ID',
|
||||
condition: { field: 'operation', value: ['get', 'invite'] },
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get', 'update', 'delete', 'move', 'instances', 'invite'],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Update Event Fields
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'New Event Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Updated meeting title',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a clear, descriptive calendar event title based on the user's request.
|
||||
The title should be concise but informative about the event's purpose.
|
||||
|
||||
Return ONLY the event title - no explanations, no extra text.`,
|
||||
placeholder: 'Describe the new event title...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'New Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'Updated event description',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a helpful calendar event description based on the user's request.
|
||||
Include relevant details like:
|
||||
- Purpose of the event
|
||||
- Agenda items
|
||||
- Preparation notes
|
||||
- Links or resources
|
||||
|
||||
Return ONLY the description - no explanations, no extra text.`,
|
||||
placeholder: 'Describe the new event details...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
title: 'New Location',
|
||||
type: 'short-input',
|
||||
placeholder: 'Updated location',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'startDateTime',
|
||||
title: 'New Start Date & Time',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-03T10:00:00-08:00',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp with timezone offset based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS+HH:MM or YYYY-MM-DDTHH:MM:SS-HH:MM
|
||||
Examples:
|
||||
- "tomorrow at 2pm" -> Calculate tomorrow's date at 14:00:00 with local timezone offset
|
||||
- "next Monday at 9am" -> Calculate next Monday at 09:00:00 with local timezone offset
|
||||
- "in 2 hours" -> Calculate current time + 2 hours with local timezone offset
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the new start time (e.g., "tomorrow at 2pm")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'endDateTime',
|
||||
title: 'New End Date & Time',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-03T11:00:00-08:00',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp with timezone offset based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SS+HH:MM or YYYY-MM-DDTHH:MM:SS-HH:MM
|
||||
Examples:
|
||||
- "tomorrow at 3pm" -> Calculate tomorrow's date at 15:00:00 with local timezone offset
|
||||
- "1 hour after start" -> Calculate start time + 1 hour with local timezone offset
|
||||
- "next Monday at 5pm" -> Calculate next Monday at 17:00:00 with local timezone offset
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the new end time (e.g., "tomorrow at 3pm")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'attendees',
|
||||
title: 'New Attendees (comma-separated emails)',
|
||||
type: 'short-input',
|
||||
placeholder: 'john@example.com, jane@example.com',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
|
||||
// Move Event Fields
|
||||
{
|
||||
id: 'destinationCalendarId',
|
||||
title: 'Destination Calendar ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'destination@group.calendar.google.com',
|
||||
condition: { field: 'operation', value: 'move' },
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Instances Fields
|
||||
{
|
||||
id: 'timeMin',
|
||||
title: 'Start Time Filter',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-03T00:00:00Z',
|
||||
condition: { field: 'operation', value: 'instances' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "today" -> Calculate today's date at 00:00:00Z
|
||||
- "yesterday" -> Calculate yesterday's date at 00:00:00Z
|
||||
- "last week" -> Calculate 7 days ago at 00:00:00Z
|
||||
- "beginning of this month" -> Calculate the first day of current month at 00:00:00Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the start of time range (e.g., "today", "last week")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'timeMax',
|
||||
title: 'End Time Filter',
|
||||
type: 'short-input',
|
||||
placeholder: '2025-06-04T00:00:00Z',
|
||||
condition: { field: 'operation', value: 'instances' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate an ISO 8601 timestamp in UTC based on the user's description.
|
||||
The timestamp should be in the format: YYYY-MM-DDTHH:MM:SSZ (UTC timezone).
|
||||
Examples:
|
||||
- "tomorrow" -> Calculate tomorrow's date at 00:00:00Z
|
||||
- "end of today" -> Calculate today's date at 23:59:59Z
|
||||
- "next week" -> Calculate 7 days from now at 00:00:00Z
|
||||
- "end of this month" -> Calculate the last day of current month at 23:59:59Z
|
||||
|
||||
Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe the end of time range (e.g., "tomorrow", "end of this week")...',
|
||||
generationType: 'timestamp',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'maxResults',
|
||||
title: 'Max Results',
|
||||
type: 'short-input',
|
||||
placeholder: '250',
|
||||
condition: { field: 'operation', value: ['instances', 'list_calendars'] },
|
||||
},
|
||||
|
||||
// List Calendars Fields
|
||||
{
|
||||
id: 'minAccessRole',
|
||||
title: 'Minimum Access Role',
|
||||
type: 'dropdown',
|
||||
condition: { field: 'operation', value: 'list_calendars' },
|
||||
options: [
|
||||
{ label: 'Any Role', id: '' },
|
||||
{ label: 'Free/Busy Reader', id: 'freeBusyReader' },
|
||||
{ label: 'Reader', id: 'reader' },
|
||||
{ label: 'Writer', id: 'writer' },
|
||||
{ label: 'Owner', id: 'owner' },
|
||||
],
|
||||
},
|
||||
|
||||
// Invite Attendees Fields
|
||||
{
|
||||
id: 'attendees',
|
||||
@@ -262,14 +438,14 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Notification setting (for create, quick_add, invite)
|
||||
// Notification setting (for create, update, delete, move, quick_add, invite)
|
||||
{
|
||||
id: 'sendUpdates',
|
||||
title: 'Send Email Notifications',
|
||||
type: 'dropdown',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create', 'quick_add', 'invite'],
|
||||
value: ['create', 'update', 'delete', 'move', 'quick_add', 'invite'],
|
||||
},
|
||||
options: [
|
||||
{ label: 'All attendees', id: 'all' },
|
||||
@@ -283,6 +459,11 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
'google_calendar_create',
|
||||
'google_calendar_list',
|
||||
'google_calendar_get',
|
||||
'google_calendar_update',
|
||||
'google_calendar_delete',
|
||||
'google_calendar_move',
|
||||
'google_calendar_instances',
|
||||
'google_calendar_list_calendars',
|
||||
'google_calendar_quick_add',
|
||||
'google_calendar_invite',
|
||||
],
|
||||
@@ -295,6 +476,16 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
return 'google_calendar_list'
|
||||
case 'get':
|
||||
return 'google_calendar_get'
|
||||
case 'update':
|
||||
return 'google_calendar_update'
|
||||
case 'delete':
|
||||
return 'google_calendar_delete'
|
||||
case 'move':
|
||||
return 'google_calendar_move'
|
||||
case 'instances':
|
||||
return 'google_calendar_instances'
|
||||
case 'list_calendars':
|
||||
return 'google_calendar_list_calendars'
|
||||
case 'quick_add':
|
||||
return 'google_calendar_quick_add'
|
||||
case 'invite':
|
||||
@@ -341,10 +532,23 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
}
|
||||
|
||||
// Set default sendUpdates to 'all' if not specified for operations that support it
|
||||
if (['create', 'quick_add', 'invite'].includes(operation) && !processedParams.sendUpdates) {
|
||||
if (
|
||||
['create', 'update', 'delete', 'move', 'quick_add', 'invite'].includes(operation) &&
|
||||
!processedParams.sendUpdates
|
||||
) {
|
||||
processedParams.sendUpdates = 'all'
|
||||
}
|
||||
|
||||
// Convert maxResults to number if provided
|
||||
if (processedParams.maxResults && typeof processedParams.maxResults === 'string') {
|
||||
processedParams.maxResults = Number.parseInt(processedParams.maxResults, 10)
|
||||
}
|
||||
|
||||
// Remove empty minAccessRole
|
||||
if (processedParams.minAccessRole === '') {
|
||||
processedParams.minAccessRole = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
credential,
|
||||
...processedParams,
|
||||
@@ -358,7 +562,7 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
calendarId: { type: 'string', description: 'Calendar identifier' },
|
||||
manualCalendarId: { type: 'string', description: 'Manual calendar identifier' },
|
||||
|
||||
// Create operation inputs
|
||||
// Create/Update operation inputs
|
||||
summary: { type: 'string', description: 'Event title' },
|
||||
description: { type: 'string', description: 'Event description' },
|
||||
location: { type: 'string', description: 'Event location' },
|
||||
@@ -366,13 +570,20 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
endDateTime: { type: 'string', description: 'Event end time' },
|
||||
attendees: { type: 'string', description: 'Attendee email list' },
|
||||
|
||||
// List operation inputs
|
||||
// List/Instances operation inputs
|
||||
timeMin: { type: 'string', description: 'Start time filter' },
|
||||
timeMax: { type: 'string', description: 'End time filter' },
|
||||
maxResults: { type: 'string', description: 'Maximum number of results' },
|
||||
|
||||
// Get/Invite operation inputs
|
||||
// Get/Update/Delete/Move/Instances/Invite operation inputs
|
||||
eventId: { type: 'string', description: 'Event identifier' },
|
||||
|
||||
// Move operation inputs
|
||||
destinationCalendarId: { type: 'string', description: 'Destination calendar ID' },
|
||||
|
||||
// List Calendars operation inputs
|
||||
minAccessRole: { type: 'string', description: 'Minimum access role filter' },
|
||||
|
||||
// Quick add inputs
|
||||
text: { type: 'string', description: 'Natural language event' },
|
||||
|
||||
@@ -384,7 +595,7 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
},
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Operation response content' },
|
||||
metadata: { type: 'json', description: 'Event metadata' },
|
||||
metadata: { type: 'json', description: 'Event or calendar metadata' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -399,6 +610,11 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
|
||||
'google_calendar_create_v2',
|
||||
'google_calendar_list_v2',
|
||||
'google_calendar_get_v2',
|
||||
'google_calendar_update_v2',
|
||||
'google_calendar_delete_v2',
|
||||
'google_calendar_move_v2',
|
||||
'google_calendar_instances_v2',
|
||||
'google_calendar_list_calendars_v2',
|
||||
'google_calendar_quick_add_v2',
|
||||
'google_calendar_invite_v2',
|
||||
],
|
||||
@@ -413,6 +629,7 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// Event outputs (create, get, update, move, quick_add, invite)
|
||||
id: { type: 'string', description: 'Event ID' },
|
||||
htmlLink: { type: 'string', description: 'Event link' },
|
||||
status: { type: 'string', description: 'Event status' },
|
||||
@@ -424,9 +641,17 @@ export const GoogleCalendarV2Block: BlockConfig<GoogleCalendarResponse> = {
|
||||
attendees: { type: 'json', description: 'Event attendees' },
|
||||
creator: { type: 'json', description: 'Event creator' },
|
||||
organizer: { type: 'json', description: 'Event organizer' },
|
||||
// List events outputs
|
||||
events: { type: 'json', description: 'List of events (list operation)' },
|
||||
// Delete outputs
|
||||
eventId: { type: 'string', description: 'Deleted event ID' },
|
||||
deleted: { type: 'boolean', description: 'Whether deletion was successful' },
|
||||
// Instances outputs
|
||||
instances: { type: 'json', description: 'List of recurring event instances' },
|
||||
// List calendars outputs
|
||||
calendars: { type: 'json', description: 'List of calendars' },
|
||||
// Common outputs
|
||||
nextPageToken: { type: 'string', description: 'Next page token' },
|
||||
nextSyncToken: { type: 'string', description: 'Next sync token' },
|
||||
timeZone: { type: 'string', description: 'Calendar time zone' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import type { GoogleDriveResponse } from '@/tools/google_drive/types'
|
||||
export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
type: 'google_drive',
|
||||
name: 'Google Drive',
|
||||
description: 'Create, upload, and list files',
|
||||
description: 'Manage files, folders, and permissions',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription: 'Integrate Google Drive into the workflow. Can create, upload, and list files.',
|
||||
longDescription:
|
||||
'Integrate Google Drive into the workflow. Can create, upload, download, copy, move, delete, share files and manage permissions.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_drive',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
@@ -20,13 +21,23 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Files', id: 'list' },
|
||||
{ label: 'Get File Info', id: 'get_file' },
|
||||
{ label: 'Create Folder', id: 'create_folder' },
|
||||
{ label: 'Create File', id: 'create_file' },
|
||||
{ label: 'Upload File', id: 'upload' },
|
||||
{ label: 'Download File', id: 'download' },
|
||||
{ label: 'List Files', id: 'list' },
|
||||
{ label: 'Copy File', id: 'copy' },
|
||||
{ label: 'Update File', id: 'update' },
|
||||
{ label: 'Move to Trash', id: 'trash' },
|
||||
{ label: 'Restore from Trash', id: 'untrash' },
|
||||
{ label: 'Delete Permanently', id: 'delete' },
|
||||
{ label: 'Share File', id: 'share' },
|
||||
{ label: 'Remove Sharing', id: 'unshare' },
|
||||
{ label: 'List Permissions', id: 'list_permissions' },
|
||||
{ label: 'Get Drive Info', id: 'get_about' },
|
||||
],
|
||||
value: () => 'create_folder',
|
||||
value: () => 'list',
|
||||
},
|
||||
// Google Drive Credentials
|
||||
{
|
||||
@@ -326,26 +337,453 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
placeholder: 'Optional: Override the filename',
|
||||
condition: { field: 'operation', value: 'download' },
|
||||
},
|
||||
// Get File Info Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to get info for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'get_file' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'get_file' },
|
||||
required: true,
|
||||
},
|
||||
// Copy File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File to Copy',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to copy',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'copy' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to copy',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'copy' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'newName',
|
||||
title: 'New File Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Name for the copy (optional)',
|
||||
condition: { field: 'operation', value: 'copy' },
|
||||
},
|
||||
{
|
||||
id: 'folderSelector',
|
||||
title: 'Destination Folder',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'destinationFolderId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
placeholder: 'Select destination folder (optional)',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'copy' },
|
||||
},
|
||||
{
|
||||
id: 'manualDestinationFolderId',
|
||||
title: 'Destination Folder ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'destinationFolderId',
|
||||
placeholder: 'Enter destination folder ID (optional)',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'copy' },
|
||||
},
|
||||
// Update File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File to Update',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to update',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to update',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
title: 'New Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'New name for the file (optional)',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'Description',
|
||||
type: 'long-input',
|
||||
placeholder: 'New description for the file (optional)',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a clear, informative file description based on the user's input.
|
||||
The description should help users understand the file's purpose and contents.
|
||||
Keep it concise but comprehensive - typically 1-3 sentences.
|
||||
|
||||
Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
placeholder: 'Describe what this file is about...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'starred',
|
||||
title: 'Starred',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'No Change', id: '' },
|
||||
{ label: 'Star', id: 'true' },
|
||||
{ label: 'Unstar', id: 'false' },
|
||||
],
|
||||
placeholder: 'Star or unstar the file',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'addParents',
|
||||
title: 'Add to Folders',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated folder IDs to add file to',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'removeParents',
|
||||
title: 'Remove from Folders',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated folder IDs to remove file from',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
// Trash File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File to Trash',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to move to trash',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'trash' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to trash',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'trash' },
|
||||
required: true,
|
||||
},
|
||||
// Untrash File Fields
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to restore from trash',
|
||||
condition: { field: 'operation', value: 'untrash' },
|
||||
required: true,
|
||||
},
|
||||
// Delete File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File to Delete',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to permanently delete',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to permanently delete',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
required: true,
|
||||
},
|
||||
// Share File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File to Share',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to share',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'share' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to share',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'share' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'shareType',
|
||||
title: 'Share With',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User (email)', id: 'user' },
|
||||
{ label: 'Group (email)', id: 'group' },
|
||||
{ label: 'Domain', id: 'domain' },
|
||||
{ label: 'Anyone with link', id: 'anyone' },
|
||||
],
|
||||
placeholder: 'Who to share with',
|
||||
condition: { field: 'operation', value: 'share' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
title: 'Permission Level',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Viewer (read only)', id: 'reader' },
|
||||
{ label: 'Commenter (view & comment)', id: 'commenter' },
|
||||
{ label: 'Editor (can edit)', id: 'writer' },
|
||||
{ label: 'Transfer Ownership', id: 'owner' },
|
||||
],
|
||||
placeholder: 'Permission level',
|
||||
condition: { field: 'operation', value: 'share' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
placeholder: 'Email of user or group to share with',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'share',
|
||||
and: { field: 'shareType', value: ['user', 'group'] },
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'domain',
|
||||
title: 'Domain',
|
||||
type: 'short-input',
|
||||
placeholder: 'Domain to share with (e.g., example.com)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'share',
|
||||
and: { field: 'shareType', value: 'domain' },
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sendNotification',
|
||||
title: 'Send Notification',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Yes (default)', id: 'true' },
|
||||
{ label: 'No', id: 'false' },
|
||||
],
|
||||
placeholder: 'Send email notification',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'share',
|
||||
and: { field: 'shareType', value: ['user', 'group'] },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'emailMessage',
|
||||
title: 'Custom Message',
|
||||
type: 'long-input',
|
||||
placeholder: 'Custom message for the notification email (optional)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'share',
|
||||
and: { field: 'shareType', value: ['user', 'group'] },
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a professional, friendly sharing notification message based on the user's input.
|
||||
The message should clearly explain why the file is being shared and any relevant context.
|
||||
Keep it concise and appropriate for a business email - typically 2-4 sentences.
|
||||
|
||||
Return ONLY the message text - no subject line, no greetings/signatures, no extra formatting.`,
|
||||
placeholder: 'Describe why you are sharing this file...',
|
||||
},
|
||||
},
|
||||
// Unshare (Remove Permission) Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to remove sharing from',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'unshare' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'unshare' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'permissionId',
|
||||
title: 'Permission ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Permission ID to remove (use List Permissions to find)',
|
||||
condition: { field: 'operation', value: 'unshare' },
|
||||
required: true,
|
||||
},
|
||||
// List Permissions Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
title: 'Select File',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'fileId',
|
||||
serviceId: 'google-drive',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select a file to list permissions for',
|
||||
mode: 'basic',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'list_permissions' },
|
||||
},
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID',
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'list_permissions' },
|
||||
required: true,
|
||||
},
|
||||
// Get Drive Info has no additional fields (just needs credential)
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'google_drive_upload',
|
||||
'google_drive_create_folder',
|
||||
'google_drive_download',
|
||||
'google_drive_list',
|
||||
'google_drive_get_file',
|
||||
'google_drive_create_folder',
|
||||
'google_drive_upload',
|
||||
'google_drive_download',
|
||||
'google_drive_copy',
|
||||
'google_drive_update',
|
||||
'google_drive_trash',
|
||||
'google_drive_untrash',
|
||||
'google_drive_delete',
|
||||
'google_drive_share',
|
||||
'google_drive_unshare',
|
||||
'google_drive_list_permissions',
|
||||
'google_drive_get_about',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'list':
|
||||
return 'google_drive_list'
|
||||
case 'get_file':
|
||||
return 'google_drive_get_file'
|
||||
case 'create_folder':
|
||||
return 'google_drive_create_folder'
|
||||
case 'create_file':
|
||||
case 'upload':
|
||||
return 'google_drive_upload'
|
||||
case 'create_folder':
|
||||
return 'google_drive_create_folder'
|
||||
case 'download':
|
||||
return 'google_drive_download'
|
||||
case 'list':
|
||||
return 'google_drive_list'
|
||||
case 'copy':
|
||||
return 'google_drive_copy'
|
||||
case 'update':
|
||||
return 'google_drive_update'
|
||||
case 'trash':
|
||||
return 'google_drive_trash'
|
||||
case 'untrash':
|
||||
return 'google_drive_untrash'
|
||||
case 'delete':
|
||||
return 'google_drive_delete'
|
||||
case 'share':
|
||||
return 'google_drive_share'
|
||||
case 'unshare':
|
||||
return 'google_drive_unshare'
|
||||
case 'list_permissions':
|
||||
return 'google_drive_list_permissions'
|
||||
case 'get_about':
|
||||
return 'google_drive_get_about'
|
||||
default:
|
||||
throw new Error(`Invalid Google Drive operation: ${params.operation}`)
|
||||
}
|
||||
@@ -355,9 +793,13 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
credential,
|
||||
folderSelector,
|
||||
manualFolderId,
|
||||
manualDestinationFolderId,
|
||||
fileSelector,
|
||||
manualFileId,
|
||||
mimeType,
|
||||
shareType,
|
||||
starred,
|
||||
sendNotification,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
@@ -367,12 +809,30 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
// Use fileSelector if provided, otherwise use manualFileId
|
||||
const effectiveFileId = (fileSelector || manualFileId || '').trim()
|
||||
|
||||
// Use folderSelector for destination or manualDestinationFolderId for copy operation
|
||||
const effectiveDestinationFolderId =
|
||||
params.operation === 'copy'
|
||||
? (folderSelector || manualDestinationFolderId || '').trim()
|
||||
: undefined
|
||||
|
||||
// Convert starred dropdown to boolean
|
||||
const starredValue = starred === 'true' ? true : starred === 'false' ? false : undefined
|
||||
|
||||
// Convert sendNotification dropdown to boolean
|
||||
const sendNotificationValue =
|
||||
sendNotification === 'true' ? true : sendNotification === 'false' ? false : undefined
|
||||
|
||||
return {
|
||||
credential,
|
||||
folderId: effectiveFolderId || undefined,
|
||||
fileId: effectiveFileId || undefined,
|
||||
destinationFolderId: effectiveDestinationFolderId || undefined,
|
||||
pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined,
|
||||
mimeType: mimeType,
|
||||
type: shareType, // Map shareType to type for share tool
|
||||
starred: starredValue,
|
||||
sendNotification: sendNotificationValue,
|
||||
transferOwnership: rest.role === 'owner' ? true : undefined,
|
||||
...rest,
|
||||
}
|
||||
},
|
||||
@@ -381,22 +841,47 @@ Return ONLY the query string - no explanations, no quotes around the whole thing
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Google Drive access token' },
|
||||
// Upload and Create Folder operation inputs
|
||||
// File selection inputs
|
||||
fileSelector: { type: 'string', description: 'Selected file' },
|
||||
manualFileId: { type: 'string', description: 'Manual file identifier' },
|
||||
// Folder selection inputs
|
||||
folderSelector: { type: 'string', description: 'Selected folder' },
|
||||
manualFolderId: { type: 'string', description: 'Manual folder identifier' },
|
||||
manualDestinationFolderId: { type: 'string', description: 'Destination folder for copy' },
|
||||
// Upload and Create inputs
|
||||
fileName: { type: 'string', description: 'File or folder name' },
|
||||
file: { type: 'json', description: 'File to upload (UserFile object)' },
|
||||
content: { type: 'string', description: 'Text content to upload' },
|
||||
mimeType: { type: 'string', description: 'File MIME type or export format' },
|
||||
// Download operation inputs
|
||||
fileSelector: { type: 'string', description: 'Selected file to download' },
|
||||
manualFileId: { type: 'string', description: 'Manual file identifier' },
|
||||
// List operation inputs
|
||||
folderSelector: { type: 'string', description: 'Selected folder' },
|
||||
manualFolderId: { type: 'string', description: 'Manual folder identifier' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
pageSize: { type: 'number', description: 'Results per page' },
|
||||
// Copy operation inputs
|
||||
newName: { type: 'string', description: 'New name for copied file' },
|
||||
// Update operation inputs
|
||||
name: { type: 'string', description: 'New name for file' },
|
||||
description: { type: 'string', description: 'New description for file' },
|
||||
starred: { type: 'string', description: 'Star or unstar the file' },
|
||||
addParents: { type: 'string', description: 'Folder IDs to add file to' },
|
||||
removeParents: { type: 'string', description: 'Folder IDs to remove file from' },
|
||||
// Share operation inputs
|
||||
shareType: { type: 'string', description: 'Type of sharing (user, group, domain, anyone)' },
|
||||
role: { type: 'string', description: 'Permission role' },
|
||||
email: { type: 'string', description: 'Email address to share with' },
|
||||
domain: { type: 'string', description: 'Domain to share with' },
|
||||
sendNotification: { type: 'string', description: 'Send notification email' },
|
||||
emailMessage: { type: 'string', description: 'Custom notification message' },
|
||||
// Unshare operation inputs
|
||||
permissionId: { type: 'string', description: 'Permission ID to remove' },
|
||||
},
|
||||
outputs: {
|
||||
file: { type: 'json', description: 'File data' },
|
||||
files: { type: 'json', description: 'Files list' },
|
||||
file: { type: 'json', description: 'File metadata' },
|
||||
files: { type: 'json', description: 'List of files' },
|
||||
permission: { type: 'json', description: 'Permission details' },
|
||||
permissions: { type: 'json', description: 'List of permissions' },
|
||||
user: { type: 'json', description: 'User information' },
|
||||
storageQuota: { type: 'json', description: 'Storage quota information' },
|
||||
deleted: { type: 'boolean', description: 'Whether file was deleted' },
|
||||
removed: { type: 'boolean', description: 'Whether permission was removed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { GoogleFormsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const GoogleFormsBlock: BlockConfig = {
|
||||
type: 'google_forms',
|
||||
name: 'Google Forms',
|
||||
description: 'Read responses from a Google Form',
|
||||
longDescription:
|
||||
'Integrate Google Forms into your workflow. Provide a Form ID to list responses, or specify a Response ID to fetch a single response. Requires OAuth.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_forms',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleFormsIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
type: 'oauth-input',
|
||||
required: true,
|
||||
serviceId: 'google-forms',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
{
|
||||
id: 'formId',
|
||||
title: 'Form ID',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter the Google Form ID',
|
||||
dependsOn: ['credential'],
|
||||
},
|
||||
{
|
||||
id: 'responseId',
|
||||
title: 'Response ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a specific response ID',
|
||||
},
|
||||
{
|
||||
id: 'pageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max responses to retrieve (default 5000)',
|
||||
},
|
||||
...getTrigger('google_forms_webhook').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
access: ['google_forms_get_responses'],
|
||||
config: {
|
||||
tool: () => 'google_forms_get_responses',
|
||||
params: (params) => {
|
||||
const { credential, formId, responseId, pageSize, ...rest } = params
|
||||
|
||||
const effectiveFormId = String(formId || '').trim()
|
||||
if (!effectiveFormId) {
|
||||
throw new Error('Form ID is required.')
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
formId: effectiveFormId,
|
||||
responseId: responseId ? String(responseId).trim() : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
credential: { type: 'string', description: 'Google OAuth credential' },
|
||||
formId: { type: 'string', description: 'Google Form ID' },
|
||||
responseId: { type: 'string', description: 'Specific response ID' },
|
||||
pageSize: { type: 'string', description: 'Max responses to retrieve (default 5000)' },
|
||||
},
|
||||
outputs: {
|
||||
data: { type: 'json', description: 'Response or list of responses' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['google_forms_webhook'],
|
||||
},
|
||||
}
|
||||
329
apps/sim/blocks/blocks/google_forms.ts
Normal file
329
apps/sim/blocks/blocks/google_forms.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import { GoogleFormsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const GoogleFormsBlock: BlockConfig = {
|
||||
type: 'google_forms',
|
||||
name: 'Google Forms',
|
||||
description: 'Manage Google Forms and responses',
|
||||
longDescription:
|
||||
'Integrate Google Forms into your workflow. Read form structure, get responses, create forms, update content, and manage notification watches.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_forms',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleFormsIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Get Responses', id: 'get_responses' },
|
||||
{ label: 'Get Form', id: 'get_form' },
|
||||
{ label: 'Create Form', id: 'create_form' },
|
||||
{ label: 'Batch Update', id: 'batch_update' },
|
||||
{ label: 'Set Publish Settings', id: 'set_publish_settings' },
|
||||
{ label: 'Create Watch', id: 'create_watch' },
|
||||
{ label: 'List Watches', id: 'list_watches' },
|
||||
{ label: 'Delete Watch', id: 'delete_watch' },
|
||||
{ label: 'Renew Watch', id: 'renew_watch' },
|
||||
],
|
||||
value: () => 'get_responses',
|
||||
},
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
type: 'oauth-input',
|
||||
required: true,
|
||||
serviceId: 'google-forms',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
// Form ID - required for most operations except create_form
|
||||
{
|
||||
id: 'formId',
|
||||
title: 'Form ID',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter the Google Form ID',
|
||||
dependsOn: ['credential'],
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_form',
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
// Get Responses specific fields
|
||||
{
|
||||
id: 'responseId',
|
||||
title: 'Response ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a specific response ID (optional)',
|
||||
condition: { field: 'operation', value: 'get_responses' },
|
||||
},
|
||||
{
|
||||
id: 'pageSize',
|
||||
title: 'Page Size',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max responses to retrieve (default 5000)',
|
||||
condition: { field: 'operation', value: 'get_responses' },
|
||||
},
|
||||
// Create Form specific fields
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Form Title',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'Enter the form title',
|
||||
condition: { field: 'operation', value: 'create_form' },
|
||||
},
|
||||
{
|
||||
id: 'documentTitle',
|
||||
title: 'Document Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Title visible in Drive (optional)',
|
||||
condition: { field: 'operation', value: 'create_form' },
|
||||
},
|
||||
{
|
||||
id: 'unpublished',
|
||||
title: 'Create Unpublished',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'create_form' },
|
||||
},
|
||||
// Batch Update specific fields
|
||||
{
|
||||
id: 'requests',
|
||||
title: 'Update Requests',
|
||||
type: 'code',
|
||||
placeholder: 'JSON array of update requests',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a Google Forms batchUpdate requests array based on the user's description.
|
||||
|
||||
The requests array can contain these operation types:
|
||||
- updateFormInfo: Update form title/description. Structure: {updateFormInfo: {info: {title?, description?}, updateMask: "title,description"}}
|
||||
- updateSettings: Update form settings. Structure: {updateSettings: {settings: {quizSettings?: {isQuiz: boolean}}, updateMask: "quizSettings.isQuiz"}}
|
||||
- createItem: Add a question/section. Structure: {createItem: {item: {title, questionItem?: {question: {required?: boolean, choiceQuestion?: {type: "RADIO"|"CHECKBOX"|"DROP_DOWN", options: [{value: string}]}, textQuestion?: {paragraph?: boolean}, scaleQuestion?: {low: number, high: number}}}}, location: {index: number}}}
|
||||
- updateItem: Modify existing item. Structure: {updateItem: {item: {...}, location: {index: number}, updateMask: "..."}}
|
||||
- moveItem: Reorder item. Structure: {moveItem: {originalLocation: {index: number}, newLocation: {index: number}}}
|
||||
- deleteItem: Remove item. Structure: {deleteItem: {location: {index: number}}}
|
||||
|
||||
Return ONLY a valid JSON array of request objects. No explanations.
|
||||
|
||||
Example for "Add a required multiple choice question about favorite color":
|
||||
[{"createItem":{"item":{"title":"What is your favorite color?","questionItem":{"question":{"required":true,"choiceQuestion":{"type":"RADIO","options":[{"value":"Red"},{"value":"Blue"},{"value":"Green"}]}}}},"location":{"index":0}}}]`,
|
||||
placeholder: 'Describe what you want to add or change in the form...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'includeFormInResponse',
|
||||
title: 'Include Form in Response',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
// Set Publish Settings specific fields
|
||||
{
|
||||
id: 'isPublished',
|
||||
title: 'Published',
|
||||
type: 'switch',
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'set_publish_settings' },
|
||||
},
|
||||
{
|
||||
id: 'isAcceptingResponses',
|
||||
title: 'Accepting Responses',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'set_publish_settings' },
|
||||
},
|
||||
// Watch specific fields
|
||||
{
|
||||
id: 'eventType',
|
||||
title: 'Event Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Form Responses', id: 'RESPONSES' },
|
||||
{ label: 'Form Schema Changes', id: 'SCHEMA' },
|
||||
],
|
||||
required: true,
|
||||
condition: { field: 'operation', value: 'create_watch' },
|
||||
},
|
||||
{
|
||||
id: 'topicName',
|
||||
title: 'Pub/Sub Topic',
|
||||
type: 'short-input',
|
||||
required: true,
|
||||
placeholder: 'projects/{project}/topics/{topic}',
|
||||
condition: { field: 'operation', value: 'create_watch' },
|
||||
},
|
||||
{
|
||||
id: 'watchId',
|
||||
title: 'Watch ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Custom watch ID (optional)',
|
||||
condition: { field: 'operation', value: ['create_watch', 'delete_watch', 'renew_watch'] },
|
||||
required: { field: 'operation', value: ['delete_watch', 'renew_watch'] },
|
||||
},
|
||||
...getTrigger('google_forms_webhook').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'google_forms_get_responses',
|
||||
'google_forms_get_form',
|
||||
'google_forms_create_form',
|
||||
'google_forms_batch_update',
|
||||
'google_forms_set_publish_settings',
|
||||
'google_forms_create_watch',
|
||||
'google_forms_list_watches',
|
||||
'google_forms_delete_watch',
|
||||
'google_forms_renew_watch',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'get_responses':
|
||||
return 'google_forms_get_responses'
|
||||
case 'get_form':
|
||||
return 'google_forms_get_form'
|
||||
case 'create_form':
|
||||
return 'google_forms_create_form'
|
||||
case 'batch_update':
|
||||
return 'google_forms_batch_update'
|
||||
case 'set_publish_settings':
|
||||
return 'google_forms_set_publish_settings'
|
||||
case 'create_watch':
|
||||
return 'google_forms_create_watch'
|
||||
case 'list_watches':
|
||||
return 'google_forms_list_watches'
|
||||
case 'delete_watch':
|
||||
return 'google_forms_delete_watch'
|
||||
case 'renew_watch':
|
||||
return 'google_forms_renew_watch'
|
||||
default:
|
||||
throw new Error(`Invalid Google Forms operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
operation,
|
||||
formId,
|
||||
responseId,
|
||||
pageSize,
|
||||
title,
|
||||
documentTitle,
|
||||
unpublished,
|
||||
requests,
|
||||
includeFormInResponse,
|
||||
isPublished,
|
||||
isAcceptingResponses,
|
||||
eventType,
|
||||
topicName,
|
||||
watchId,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const baseParams = { ...rest, credential }
|
||||
const effectiveFormId = formId ? String(formId).trim() : undefined
|
||||
|
||||
switch (operation) {
|
||||
case 'get_responses':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
return {
|
||||
...baseParams,
|
||||
formId: effectiveFormId,
|
||||
responseId: responseId ? String(responseId).trim() : undefined,
|
||||
pageSize: pageSize ? Number(pageSize) : undefined,
|
||||
}
|
||||
case 'get_form':
|
||||
case 'list_watches':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
return { ...baseParams, formId: effectiveFormId }
|
||||
case 'create_form':
|
||||
if (!title) throw new Error('Form title is required.')
|
||||
return {
|
||||
...baseParams,
|
||||
title: String(title).trim(),
|
||||
documentTitle: documentTitle ? String(documentTitle).trim() : undefined,
|
||||
unpublished: unpublished ?? false,
|
||||
}
|
||||
case 'batch_update':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
if (!requests) throw new Error('Update requests are required.')
|
||||
return {
|
||||
...baseParams,
|
||||
formId: effectiveFormId,
|
||||
requests: typeof requests === 'string' ? JSON.parse(requests) : requests,
|
||||
includeFormInResponse: includeFormInResponse ?? false,
|
||||
}
|
||||
case 'set_publish_settings':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
return {
|
||||
...baseParams,
|
||||
formId: effectiveFormId,
|
||||
isPublished: isPublished ?? false,
|
||||
isAcceptingResponses: isAcceptingResponses,
|
||||
}
|
||||
case 'create_watch':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
if (!eventType) throw new Error('Event type is required.')
|
||||
if (!topicName) throw new Error('Pub/Sub topic is required.')
|
||||
return {
|
||||
...baseParams,
|
||||
formId: effectiveFormId,
|
||||
eventType: String(eventType),
|
||||
topicName: String(topicName).trim(),
|
||||
watchId: watchId ? String(watchId).trim() : undefined,
|
||||
}
|
||||
case 'delete_watch':
|
||||
case 'renew_watch':
|
||||
if (!effectiveFormId) throw new Error('Form ID is required.')
|
||||
if (!watchId) throw new Error('Watch ID is required.')
|
||||
return {
|
||||
...baseParams,
|
||||
formId: effectiveFormId,
|
||||
watchId: String(watchId).trim(),
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid Google Forms operation: ${operation}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Google OAuth credential' },
|
||||
formId: { type: 'string', description: 'Google Form ID' },
|
||||
responseId: { type: 'string', description: 'Specific response ID' },
|
||||
pageSize: { type: 'string', description: 'Max responses to retrieve' },
|
||||
title: { type: 'string', description: 'Form title for creation' },
|
||||
documentTitle: { type: 'string', description: 'Document title in Drive' },
|
||||
unpublished: { type: 'boolean', description: 'Create as unpublished' },
|
||||
requests: { type: 'json', description: 'Batch update requests' },
|
||||
includeFormInResponse: { type: 'boolean', description: 'Include form in response' },
|
||||
isPublished: { type: 'boolean', description: 'Form published state' },
|
||||
isAcceptingResponses: { type: 'boolean', description: 'Form accepting responses' },
|
||||
eventType: { type: 'string', description: 'Watch event type' },
|
||||
topicName: { type: 'string', description: 'Pub/Sub topic name' },
|
||||
watchId: { type: 'string', description: 'Watch ID' },
|
||||
},
|
||||
outputs: {
|
||||
response: { type: 'json', description: 'Operation response data' },
|
||||
formId: { type: 'string', description: 'Form ID' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
responderUri: { type: 'string', description: 'Form responder URL' },
|
||||
items: { type: 'json', description: 'Form items' },
|
||||
responses: { type: 'json', description: 'Form responses' },
|
||||
watches: { type: 'json', description: 'Form watches' },
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: ['google_forms_webhook'],
|
||||
},
|
||||
}
|
||||
@@ -30,6 +30,11 @@ export const GoogleGroupsBlock: BlockConfig = {
|
||||
{ label: 'Update Member Role', id: 'update_member' },
|
||||
{ label: 'Remove Member', id: 'remove_member' },
|
||||
{ label: 'Check Membership', id: 'has_member' },
|
||||
{ label: 'List Aliases', id: 'list_aliases' },
|
||||
{ label: 'Add Alias', id: 'add_alias' },
|
||||
{ label: 'Remove Alias', id: 'remove_alias' },
|
||||
{ label: 'Get Settings', id: 'get_settings' },
|
||||
{ label: 'Update Settings', id: 'update_settings' },
|
||||
],
|
||||
value: () => 'list_groups',
|
||||
},
|
||||
@@ -112,10 +117,37 @@ Return ONLY the query string - no explanations, no quotes, no extra text.`,
|
||||
'update_member',
|
||||
'remove_member',
|
||||
'has_member',
|
||||
'list_aliases',
|
||||
'add_alias',
|
||||
'remove_alias',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'groupEmail',
|
||||
title: 'Group Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'group@example.com',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_settings', 'update_settings'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'alias',
|
||||
title: 'Alias Email',
|
||||
type: 'short-input',
|
||||
placeholder: 'alias@example.com',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['add_alias', 'remove_alias'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'email',
|
||||
title: 'Group Email',
|
||||
@@ -233,6 +265,11 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
'google_groups_remove_member',
|
||||
'google_groups_update_member',
|
||||
'google_groups_has_member',
|
||||
'google_groups_list_aliases',
|
||||
'google_groups_add_alias',
|
||||
'google_groups_remove_alias',
|
||||
'google_groups_get_settings',
|
||||
'google_groups_update_settings',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -259,6 +296,16 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
return 'google_groups_remove_member'
|
||||
case 'has_member':
|
||||
return 'google_groups_has_member'
|
||||
case 'list_aliases':
|
||||
return 'google_groups_list_aliases'
|
||||
case 'add_alias':
|
||||
return 'google_groups_add_alias'
|
||||
case 'remove_alias':
|
||||
return 'google_groups_remove_alias'
|
||||
case 'get_settings':
|
||||
return 'google_groups_get_settings'
|
||||
case 'update_settings':
|
||||
return 'google_groups_update_settings'
|
||||
default:
|
||||
throw new Error(`Invalid Google Groups operation: ${params.operation}`)
|
||||
}
|
||||
@@ -330,6 +377,33 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
groupKey: rest.groupKey,
|
||||
memberKey: rest.memberKey,
|
||||
}
|
||||
case 'list_aliases':
|
||||
return {
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
}
|
||||
case 'add_alias':
|
||||
return {
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
alias: rest.alias,
|
||||
}
|
||||
case 'remove_alias':
|
||||
return {
|
||||
credential,
|
||||
groupKey: rest.groupKey,
|
||||
alias: rest.alias,
|
||||
}
|
||||
case 'get_settings':
|
||||
return {
|
||||
credential,
|
||||
groupEmail: rest.groupEmail,
|
||||
}
|
||||
case 'update_settings':
|
||||
return {
|
||||
credential,
|
||||
groupEmail: rest.groupEmail,
|
||||
}
|
||||
default:
|
||||
return { credential, ...rest }
|
||||
}
|
||||
@@ -353,6 +427,8 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
memberEmail: { type: 'string', description: 'Email of member to add' },
|
||||
role: { type: 'string', description: 'Member role (MEMBER, MANAGER, OWNER)' },
|
||||
roles: { type: 'string', description: 'Filter by roles for list members' },
|
||||
alias: { type: 'string', description: 'Alias email address' },
|
||||
groupEmail: { type: 'string', description: 'Group email address for settings operations' },
|
||||
},
|
||||
outputs: {
|
||||
groups: { type: 'json', description: 'Array of group objects (for list_groups)' },
|
||||
@@ -362,5 +438,8 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
isMember: { type: 'boolean', description: 'Membership check result (for has_member)' },
|
||||
message: { type: 'string', description: 'Success message (for delete/remove operations)' },
|
||||
nextPageToken: { type: 'string', description: 'Token for fetching next page of results' },
|
||||
aliases: { type: 'json', description: 'Array of alias objects (for list_aliases)' },
|
||||
settings: { type: 'json', description: 'Group settings object (for get/update_settings)' },
|
||||
deleted: { type: 'boolean', description: 'Deletion result (for remove_alias)' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GoogleSheetsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleSheetsResponse } from '@/tools/google_sheets/types'
|
||||
import type { GoogleSheetsResponse, GoogleSheetsV2Response } from '@/tools/google_sheets/types'
|
||||
|
||||
// Legacy block - hidden from toolbar
|
||||
export const GoogleSheetsBlock: BlockConfig<GoogleSheetsResponse> = {
|
||||
@@ -284,3 +284,725 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
tableRange: { type: 'string', description: 'Table range' },
|
||||
},
|
||||
}
|
||||
|
||||
export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
type: 'google_sheets_v2',
|
||||
name: 'Google Sheets',
|
||||
description: 'Read, write, and update data with sheet selection',
|
||||
authMode: AuthMode.OAuth,
|
||||
hideFromToolbar: false,
|
||||
longDescription:
|
||||
'Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, update, clear data, create spreadsheets, get spreadsheet info, and copy sheets.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_sheets',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleSheetsIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Read Data', id: 'read' },
|
||||
{ label: 'Write Data', id: 'write' },
|
||||
{ label: 'Update Data', id: 'update' },
|
||||
{ label: 'Append Data', id: 'append' },
|
||||
{ label: 'Clear Data', id: 'clear' },
|
||||
{ label: 'Get Spreadsheet Info', id: 'get_info' },
|
||||
{ label: 'Create Spreadsheet', id: 'create' },
|
||||
{ label: 'Batch Read', id: 'batch_get' },
|
||||
{ label: 'Batch Update', id: 'batch_update' },
|
||||
{ label: 'Batch Clear', id: 'batch_clear' },
|
||||
{ label: 'Copy Sheet', id: 'copy_sheet' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Google Sheets Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
type: 'oauth-input',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
// Spreadsheet Selector (basic mode) - not for create operation
|
||||
{
|
||||
id: 'spreadsheetId',
|
||||
title: 'Select Spreadsheet',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: 'create', not: true },
|
||||
},
|
||||
// Manual Spreadsheet ID (advanced mode) - not for create operation
|
||||
{
|
||||
id: 'manualSpreadsheetId',
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'ID of the spreadsheet (from URL)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: 'create', not: true },
|
||||
},
|
||||
// Sheet Name Selector (basic mode) - for operations that need sheet name
|
||||
{
|
||||
id: 'sheetName',
|
||||
title: 'Sheet (Tab)',
|
||||
type: 'sheet-selector',
|
||||
canonicalParamId: 'sheetName',
|
||||
serviceId: 'google-sheets',
|
||||
placeholder: 'Select a sheet',
|
||||
required: true,
|
||||
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
|
||||
mode: 'basic',
|
||||
condition: { field: 'operation', value: ['read', 'write', 'update', 'append', 'clear'] },
|
||||
},
|
||||
// Manual Sheet Name (advanced mode) - for operations that need sheet name
|
||||
{
|
||||
id: 'manualSheetName',
|
||||
title: 'Sheet Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'sheetName',
|
||||
placeholder: 'Name of the sheet/tab (e.g., Sheet1)',
|
||||
required: true,
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: { field: 'operation', value: ['read', 'write', 'update', 'append', 'clear'] },
|
||||
},
|
||||
// Cell Range (optional for read/write/update/clear)
|
||||
{
|
||||
id: 'cellRange',
|
||||
title: 'Cell Range',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cell range (e.g., A1:D10). Defaults to A1 for write.',
|
||||
condition: { field: 'operation', value: ['read', 'write', 'update', 'clear'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a valid cell range based on the user's description.
|
||||
|
||||
### VALID FORMATS
|
||||
- Single cell: A1
|
||||
- Range: A1:D10
|
||||
- Entire column: A:A
|
||||
- Entire row: 1:1
|
||||
- Multiple columns: A:D
|
||||
- Multiple rows: 1:10
|
||||
|
||||
### RANGE RULES
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc.
|
||||
- Row numbers start at 1 (not 0)
|
||||
|
||||
### EXAMPLES
|
||||
- "first 100 rows" -> A1:Z100
|
||||
- "cells A1 through C50" -> A1:C50
|
||||
- "column A" -> A:A
|
||||
- "just the headers row" -> 1:1
|
||||
- "first cell" -> A1
|
||||
|
||||
Return ONLY the range string - no sheet name, no explanations, no quotes.`,
|
||||
placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...',
|
||||
},
|
||||
},
|
||||
// Write-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]]
|
||||
- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to write...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: ['write', 'batch_update'] },
|
||||
},
|
||||
// Update-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "update with new prices" -> [["Product", "Price"], ["Widget A", 29.99], ["Widget B", 49.99]]
|
||||
- "quarterly targets" -> [{"Q1": 10000, "Q2": 12000, "Q3": 15000, "Q4": 18000}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to update...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Append-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects',
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Value1", "Value2"], ["Value3", "Value4"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "add new sales record" -> [["2024-01-15", "Widget Pro", 5, 249.99]]
|
||||
- "append customer info" -> [{"name": "Acme Corp", "contact": "John Smith", "status": "Active"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to append...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'insertDataOption',
|
||||
title: 'Insert Data Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Insert Rows (Add new rows)', id: 'INSERT_ROWS' },
|
||||
{ label: 'Overwrite (Add to existing data)', id: 'OVERWRITE' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
},
|
||||
// Create Spreadsheet Fields
|
||||
{
|
||||
id: 'title',
|
||||
title: 'Spreadsheet Title',
|
||||
type: 'short-input',
|
||||
placeholder: 'Title for the new spreadsheet',
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sheetTitles',
|
||||
title: 'Sheet Names',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated sheet names (e.g., Sheet1, Data, Summary)',
|
||||
condition: { field: 'operation', value: 'create' },
|
||||
},
|
||||
// Batch Get Fields
|
||||
{
|
||||
id: 'ranges',
|
||||
title: 'Ranges',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON array of ranges to read (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Include sheet name in each range.',
|
||||
condition: { field: 'operation', value: 'batch_get' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON array of Google Sheets ranges based on the user's description.
|
||||
|
||||
### FORMAT
|
||||
Return a JSON array of range strings. Each range must include the sheet name.
|
||||
Format: ["SheetName!CellRange", "SheetName!CellRange", ...]
|
||||
|
||||
### RANGE RULES
|
||||
- Always include sheet name: Sheet1!A1:D10 (not just A1:D10)
|
||||
- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB
|
||||
- Row numbers start at 1
|
||||
- For entire column: Sheet1!A:A
|
||||
- For entire row: Sheet1!1:1
|
||||
|
||||
### EXAMPLES
|
||||
- "all data from Sales and the summary from Reports" -> ["Sales!A1:Z1000", "Reports!A1:D20"]
|
||||
- "first 100 rows from Sheet1 and Sheet2" -> ["Sheet1!A1:Z100", "Sheet2!A1:Z100"]
|
||||
- "headers from all three sheets" -> ["Sheet1!1:1", "Sheet2!1:1", "Sheet3!1:1"]
|
||||
- "column A from Products and Orders" -> ["Products!A:A", "Orders!A:A"]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder:
|
||||
'Describe the ranges you want to read (e.g., "all data from Sales and summary from Reports")...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Batch Update Fields
|
||||
{
|
||||
id: 'batchData',
|
||||
title: 'Data',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON array of {range, values} objects (e.g., [{"range": "Sheet1!A1:B2", "values": [["A","B"],["C","D"]]}])',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON array of data updates for Google Sheets based on the user's description.
|
||||
|
||||
### FORMAT
|
||||
Return a JSON array where each item has:
|
||||
- "range": The target range including sheet name (e.g., "Sheet1!A1:B2")
|
||||
- "values": A 2D array of values to write
|
||||
|
||||
Format: [{"range": "SheetName!CellRange", "values": [[row1], [row2], ...]}, ...]
|
||||
|
||||
### RANGE RULES
|
||||
- Always include sheet name: Sheet1!A1:D10
|
||||
- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10
|
||||
- The range size should match the values array dimensions
|
||||
|
||||
### EXAMPLES
|
||||
- "set headers to Name, Email, Phone in Sheet1 and Status, Date in Sheet2" ->
|
||||
[{"range": "Sheet1!A1:C1", "values": [["Name", "Email", "Phone"]]}, {"range": "Sheet2!A1:B1", "values": [["Status", "Date"]]}]
|
||||
|
||||
- "add totals row in A10 of Sales with formula" ->
|
||||
[{"range": "Sales!A10:B10", "values": [["Total", "=SUM(B1:B9)"]]}]
|
||||
|
||||
- "update the first three rows of data in Products" ->
|
||||
[{"range": "Products!A2:C4", "values": [["Widget", 10, 29.99], ["Gadget", 5, 49.99], ["Tool", 20, 9.99]]}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder:
|
||||
'Describe the updates (e.g., "set headers in Sheet1 and add totals in Sheet2")...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Batch Clear Fields
|
||||
{
|
||||
id: 'ranges',
|
||||
title: 'Ranges to Clear',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'JSON array of ranges to clear (e.g., ["Sheet1!A1:D10", "Sheet2!A1:B5"]). Include sheet name in each range.',
|
||||
condition: { field: 'operation', value: 'batch_clear' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a JSON array of Google Sheets ranges to clear based on the user's description.
|
||||
|
||||
### FORMAT
|
||||
Return a JSON array of range strings. Each range must include the sheet name.
|
||||
Format: ["SheetName!CellRange", "SheetName!CellRange", ...]
|
||||
|
||||
### RANGE RULES
|
||||
- Always include sheet name: Sheet1!A1:D10 (not just A1:D10)
|
||||
- Sheet names with spaces must be quoted: 'My Sheet'!A1:B10
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB
|
||||
- Row numbers start at 1
|
||||
- For entire column: Sheet1!A:A
|
||||
- For entire row: Sheet1!1:1
|
||||
- For entire sheet: Sheet1!A:ZZ (or use large range)
|
||||
|
||||
### EXAMPLES
|
||||
- "clear all data from Sales and Reports" -> ["Sales!A1:ZZ10000", "Reports!A1:ZZ10000"]
|
||||
- "clear rows 2-100 from Sheet1 and Sheet2, keep headers" -> ["Sheet1!A2:ZZ100", "Sheet2!A2:ZZ100"]
|
||||
- "clear column A from Products and Orders" -> ["Products!A:A", "Orders!A:A"]
|
||||
- "clear the summary section in Reports" -> ["Reports!A1:D20"]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder:
|
||||
'Describe the ranges to clear (e.g., "clear all data from Sales and Reports, keep headers")...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
// Copy Sheet Fields
|
||||
{
|
||||
id: 'sheetId',
|
||||
title: 'Sheet ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Numeric ID of the sheet to copy (use Get Spreadsheet Info to find IDs)',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'destinationSpreadsheetId',
|
||||
title: 'Destination Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'ID of the spreadsheet to copy to',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'google_sheets_read_v2',
|
||||
'google_sheets_write_v2',
|
||||
'google_sheets_update_v2',
|
||||
'google_sheets_append_v2',
|
||||
'google_sheets_clear_v2',
|
||||
'google_sheets_get_spreadsheet_v2',
|
||||
'google_sheets_create_spreadsheet_v2',
|
||||
'google_sheets_batch_get_v2',
|
||||
'google_sheets_batch_update_v2',
|
||||
'google_sheets_batch_clear_v2',
|
||||
'google_sheets_copy_sheet_v2',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read':
|
||||
return 'google_sheets_read_v2'
|
||||
case 'write':
|
||||
return 'google_sheets_write_v2'
|
||||
case 'update':
|
||||
return 'google_sheets_update_v2'
|
||||
case 'append':
|
||||
return 'google_sheets_append_v2'
|
||||
case 'clear':
|
||||
return 'google_sheets_clear_v2'
|
||||
case 'get_info':
|
||||
return 'google_sheets_get_spreadsheet_v2'
|
||||
case 'create':
|
||||
return 'google_sheets_create_spreadsheet_v2'
|
||||
case 'batch_get':
|
||||
return 'google_sheets_batch_get_v2'
|
||||
case 'batch_update':
|
||||
return 'google_sheets_batch_update_v2'
|
||||
case 'batch_clear':
|
||||
return 'google_sheets_batch_clear_v2'
|
||||
case 'copy_sheet':
|
||||
return 'google_sheets_copy_sheet_v2'
|
||||
default:
|
||||
throw new Error(`Invalid Google Sheets V2 operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
values,
|
||||
spreadsheetId,
|
||||
manualSpreadsheetId,
|
||||
sheetName,
|
||||
manualSheetName,
|
||||
cellRange,
|
||||
title,
|
||||
sheetTitles,
|
||||
ranges,
|
||||
batchData,
|
||||
sheetId,
|
||||
destinationSpreadsheetId,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const operation = params.operation as string
|
||||
|
||||
// Handle create operation
|
||||
if (operation === 'create') {
|
||||
const sheetTitlesArray = sheetTitles
|
||||
? (sheetTitles as string).split(',').map((s: string) => s.trim())
|
||||
: undefined
|
||||
return {
|
||||
title: (title as string)?.trim(),
|
||||
sheetTitles: sheetTitlesArray,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveSpreadsheetId = (
|
||||
(spreadsheetId || manualSpreadsheetId || '') as string
|
||||
).trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
// Handle get_info operation
|
||||
if (operation === 'get_info') {
|
||||
return {
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle batch_get operation
|
||||
if (operation === 'batch_get') {
|
||||
const parsedRanges = ranges ? JSON.parse(ranges as string) : []
|
||||
return {
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
ranges: parsedRanges,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle batch_update operation
|
||||
if (operation === 'batch_update') {
|
||||
const parsedData = batchData ? JSON.parse(batchData as string) : []
|
||||
return {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
data: parsedData,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle batch_clear operation
|
||||
if (operation === 'batch_clear') {
|
||||
const parsedRanges = ranges ? JSON.parse(ranges as string) : []
|
||||
return {
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
ranges: parsedRanges,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle copy_sheet operation
|
||||
if (operation === 'copy_sheet') {
|
||||
return {
|
||||
sourceSpreadsheetId: effectiveSpreadsheetId,
|
||||
sheetId: Number.parseInt(sheetId as string, 10),
|
||||
destinationSpreadsheetId: (destinationSpreadsheetId as string)?.trim(),
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle read/write/update/append/clear operations (require sheet name)
|
||||
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
|
||||
|
||||
if (!effectiveSheetName) {
|
||||
throw new Error('Sheet name is required. Please select or enter a sheet name.')
|
||||
}
|
||||
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
return {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
sheetName: effectiveSheetName,
|
||||
cellRange: cellRange ? (cellRange as string).trim() : undefined,
|
||||
values: parsedValues,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Google Sheets access token' },
|
||||
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
|
||||
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
|
||||
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
|
||||
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
|
||||
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
|
||||
values: { type: 'string', description: 'Cell values data' },
|
||||
valueInputOption: { type: 'string', description: 'Value input option' },
|
||||
insertDataOption: { type: 'string', description: 'Data insertion option' },
|
||||
title: { type: 'string', description: 'Title for new spreadsheet' },
|
||||
sheetTitles: { type: 'string', description: 'Comma-separated sheet names for new spreadsheet' },
|
||||
ranges: { type: 'string', description: 'JSON array of ranges for batch operations' },
|
||||
batchData: { type: 'string', description: 'JSON array of data for batch update' },
|
||||
sheetId: { type: 'string', description: 'Numeric sheet ID for copy operation' },
|
||||
destinationSpreadsheetId: {
|
||||
type: 'string',
|
||||
description: 'Destination spreadsheet ID for copy',
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
// Read outputs
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
description: 'Name of the sheet',
|
||||
condition: { field: 'operation', value: ['read', 'clear'] },
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
description: 'Range that was read',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description: 'Cell values as 2D array',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
// Write/Update/Append outputs
|
||||
updatedRange: {
|
||||
type: 'string',
|
||||
description: 'Updated range',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedRows: {
|
||||
type: 'number',
|
||||
description: 'Updated rows count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Updated columns count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedCells: {
|
||||
type: 'number',
|
||||
description: 'Updated cells count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
tableRange: {
|
||||
type: 'string',
|
||||
description: 'Table range',
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
},
|
||||
// Clear outputs
|
||||
clearedRange: {
|
||||
type: 'string',
|
||||
description: 'Range that was cleared',
|
||||
condition: { field: 'operation', value: 'clear' },
|
||||
},
|
||||
// Get Info / Create / Batch outputs
|
||||
spreadsheetId: {
|
||||
type: 'string',
|
||||
description: 'Spreadsheet ID',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['get_info', 'create', 'batch_get', 'batch_update', 'batch_clear'],
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Spreadsheet title (or copied sheet title for copy_sheet)',
|
||||
condition: { field: 'operation', value: ['get_info', 'create', 'copy_sheet'] },
|
||||
},
|
||||
sheets: {
|
||||
type: 'json',
|
||||
description: 'List of sheets in the spreadsheet',
|
||||
condition: { field: 'operation', value: ['get_info', 'create'] },
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
description: 'Spreadsheet locale',
|
||||
condition: { field: 'operation', value: 'get_info' },
|
||||
},
|
||||
timeZone: {
|
||||
type: 'string',
|
||||
description: 'Spreadsheet time zone',
|
||||
condition: { field: 'operation', value: 'get_info' },
|
||||
},
|
||||
spreadsheetUrl: {
|
||||
type: 'string',
|
||||
description: 'Spreadsheet URL',
|
||||
condition: { field: 'operation', value: ['get_info', 'create'] },
|
||||
},
|
||||
// Batch Get outputs
|
||||
valueRanges: {
|
||||
type: 'json',
|
||||
description: 'Array of value ranges read from the spreadsheet',
|
||||
condition: { field: 'operation', value: 'batch_get' },
|
||||
},
|
||||
// Batch Update outputs
|
||||
totalUpdatedRows: {
|
||||
type: 'number',
|
||||
description: 'Total rows updated',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
totalUpdatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Total columns updated',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
totalUpdatedCells: {
|
||||
type: 'number',
|
||||
description: 'Total cells updated',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
totalUpdatedSheets: {
|
||||
type: 'number',
|
||||
description: 'Total sheets updated',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
responses: {
|
||||
type: 'json',
|
||||
description: 'Array of update responses for each range',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
// Batch Clear outputs
|
||||
clearedRanges: {
|
||||
type: 'json',
|
||||
description: 'Array of ranges that were cleared',
|
||||
condition: { field: 'operation', value: 'batch_clear' },
|
||||
},
|
||||
// Copy Sheet outputs
|
||||
sheetId: {
|
||||
type: 'number',
|
||||
description: 'ID of the copied sheet in the destination',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
description: 'Position/index of the copied sheet',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
},
|
||||
sheetType: {
|
||||
type: 'string',
|
||||
description: 'Type of the sheet (GRID, CHART, etc.)',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
},
|
||||
destinationSpreadsheetId: {
|
||||
type: 'string',
|
||||
description: 'ID of the destination spreadsheet',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
},
|
||||
destinationSpreadsheetUrl: {
|
||||
type: 'string',
|
||||
description: 'URL of the destination spreadsheet',
|
||||
condition: { field: 'operation', value: 'copy_sheet' },
|
||||
},
|
||||
// Common metadata
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Spreadsheet metadata including ID and URL',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'read',
|
||||
'write',
|
||||
'update',
|
||||
'append',
|
||||
'clear',
|
||||
'batch_get',
|
||||
'batch_update',
|
||||
'batch_clear',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
import { GoogleSheetsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { GoogleSheetsV2Response } from '@/tools/google_sheets/types'
|
||||
|
||||
export const GoogleSheetsV2Block: BlockConfig<GoogleSheetsV2Response> = {
|
||||
type: 'google_sheets_v2',
|
||||
name: 'Google Sheets',
|
||||
description: 'Read, write, and update data with sheet selection',
|
||||
authMode: AuthMode.OAuth,
|
||||
hideFromToolbar: false,
|
||||
longDescription:
|
||||
'Integrate Google Sheets into the workflow with explicit sheet selection. Can read, write, append, and update data in specific sheets.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_sheets',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: GoogleSheetsIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Read Data', id: 'read' },
|
||||
{ label: 'Write Data', id: 'write' },
|
||||
{ label: 'Update Data', id: 'update' },
|
||||
{ label: 'Append Data', id: 'append' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Google Sheets Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Google Account',
|
||||
type: 'oauth-input',
|
||||
required: true,
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
// Spreadsheet Selector (basic mode)
|
||||
{
|
||||
id: 'spreadsheetId',
|
||||
title: 'Select Spreadsheet',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'google-sheets',
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
],
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Spreadsheet ID (advanced mode)
|
||||
{
|
||||
id: 'manualSpreadsheetId',
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'ID of the spreadsheet (from URL)',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Sheet Name Selector (basic mode)
|
||||
{
|
||||
id: 'sheetName',
|
||||
title: 'Sheet (Tab)',
|
||||
type: 'sheet-selector',
|
||||
canonicalParamId: 'sheetName',
|
||||
serviceId: 'google-sheets',
|
||||
placeholder: 'Select a sheet',
|
||||
required: true,
|
||||
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Sheet Name (advanced mode)
|
||||
{
|
||||
id: 'manualSheetName',
|
||||
title: 'Sheet Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'sheetName',
|
||||
placeholder: 'Name of the sheet/tab (e.g., Sheet1)',
|
||||
required: true,
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Cell Range (optional for read/write/update)
|
||||
{
|
||||
id: 'cellRange',
|
||||
title: 'Cell Range',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cell range (e.g., A1:D10). Defaults to A1 for write.',
|
||||
condition: { field: 'operation', value: ['read', 'write', 'update'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a valid cell range based on the user's description.
|
||||
|
||||
### VALID FORMATS
|
||||
- Single cell: A1
|
||||
- Range: A1:D10
|
||||
- Entire column: A:A
|
||||
- Entire row: 1:1
|
||||
- Multiple columns: A:D
|
||||
- Multiple rows: 1:10
|
||||
|
||||
### RANGE RULES
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc.
|
||||
- Row numbers start at 1 (not 0)
|
||||
|
||||
### EXAMPLES
|
||||
- "first 100 rows" -> A1:Z100
|
||||
- "cells A1 through C50" -> A1:C50
|
||||
- "column A" -> A:A
|
||||
- "just the headers row" -> 1:1
|
||||
- "first cell" -> A1
|
||||
|
||||
Return ONLY the range string - no sheet name, no explanations, no quotes.`,
|
||||
placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...',
|
||||
},
|
||||
},
|
||||
// Write-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]]
|
||||
- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to write...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
// Update-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "update with new prices" -> [["Product", "Price"], ["Widget A", 29.99], ["Widget B", 49.99]]
|
||||
- "quarterly targets" -> [{"Q1": 10000, "Q2": 12000, "Q3": 15000, "Q4": 18000}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to update...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
// Append-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects',
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Google Sheets data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Value1", "Value2"], ["Value3", "Value4"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "add new sales record" -> [["2024-01-15", "Widget Pro", 5, 249.99]]
|
||||
- "append customer info" -> [{"name": "Acme Corp", "contact": "John Smith", "status": "Active"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to append...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
},
|
||||
{
|
||||
id: 'insertDataOption',
|
||||
title: 'Insert Data Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Insert Rows (Add new rows)', id: 'INSERT_ROWS' },
|
||||
{ label: 'Overwrite (Add to existing data)', id: 'OVERWRITE' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'google_sheets_read_v2',
|
||||
'google_sheets_write_v2',
|
||||
'google_sheets_update_v2',
|
||||
'google_sheets_append_v2',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read':
|
||||
return 'google_sheets_read_v2'
|
||||
case 'write':
|
||||
return 'google_sheets_write_v2'
|
||||
case 'update':
|
||||
return 'google_sheets_update_v2'
|
||||
case 'append':
|
||||
return 'google_sheets_append_v2'
|
||||
default:
|
||||
throw new Error(`Invalid Google Sheets V2 operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
values,
|
||||
spreadsheetId,
|
||||
manualSpreadsheetId,
|
||||
sheetName,
|
||||
manualSheetName,
|
||||
cellRange,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
if (!effectiveSheetName) {
|
||||
throw new Error('Sheet name is required. Please select or enter a sheet name.')
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
sheetName: effectiveSheetName,
|
||||
cellRange: cellRange ? (cellRange as string).trim() : undefined,
|
||||
values: parsedValues,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Google Sheets access token' },
|
||||
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
|
||||
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
|
||||
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
|
||||
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
|
||||
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
|
||||
values: { type: 'string', description: 'Cell values data' },
|
||||
valueInputOption: { type: 'string', description: 'Value input option' },
|
||||
insertDataOption: { type: 'string', description: 'Data insertion option' },
|
||||
},
|
||||
outputs: {
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
description: 'Name of the sheet',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
description: 'Range that was read',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description: 'Cell values as 2D array',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
updatedRange: {
|
||||
type: 'string',
|
||||
description: 'Updated range',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedRows: {
|
||||
type: 'number',
|
||||
description: 'Updated rows count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Updated columns count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
updatedCells: {
|
||||
type: 'number',
|
||||
description: 'Updated cells count',
|
||||
condition: { field: 'operation', value: ['write', 'update', 'append'] },
|
||||
},
|
||||
tableRange: {
|
||||
type: 'string',
|
||||
description: 'Table range',
|
||||
condition: { field: 'operation', value: 'append' },
|
||||
},
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
description: 'Read, write, and create presentations',
|
||||
authMode: AuthMode.OAuth,
|
||||
longDescription:
|
||||
'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, and get thumbnails.',
|
||||
'Integrate Google Slides into the workflow. Can read, write, create presentations, replace text, add slides, add images, get thumbnails, get page details, delete objects, duplicate objects, reorder slides, create tables, create shapes, and insert text.',
|
||||
docsLink: 'https://docs.sim.ai/tools/google_slides',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
@@ -28,6 +28,13 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
{ label: 'Add Slide', id: 'add_slide' },
|
||||
{ label: 'Add Image', id: 'add_image' },
|
||||
{ label: 'Get Thumbnail', id: 'get_thumbnail' },
|
||||
{ label: 'Get Page', id: 'get_page' },
|
||||
{ label: 'Delete Object', id: 'delete_object' },
|
||||
{ label: 'Duplicate Object', id: 'duplicate_object' },
|
||||
{ label: 'Reorder Slides', id: 'reorder_slides' },
|
||||
{ label: 'Create Table', id: 'create_table' },
|
||||
{ label: 'Create Shape', id: 'create_shape' },
|
||||
{ label: 'Insert Text', id: 'insert_text' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
@@ -58,7 +65,21 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'],
|
||||
value: [
|
||||
'read',
|
||||
'write',
|
||||
'replace_all_text',
|
||||
'add_slide',
|
||||
'add_image',
|
||||
'get_thumbnail',
|
||||
'get_page',
|
||||
'delete_object',
|
||||
'duplicate_object',
|
||||
'reorder_slides',
|
||||
'create_table',
|
||||
'create_shape',
|
||||
'insert_text',
|
||||
],
|
||||
},
|
||||
},
|
||||
// Manual presentation ID input (advanced mode)
|
||||
@@ -72,7 +93,21 @@ export const GoogleSlidesBlock: BlockConfig<GoogleSlidesResponse> = {
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['read', 'write', 'replace_all_text', 'add_slide', 'add_image', 'get_thumbnail'],
|
||||
value: [
|
||||
'read',
|
||||
'write',
|
||||
'replace_all_text',
|
||||
'add_slide',
|
||||
'add_image',
|
||||
'get_thumbnail',
|
||||
'get_page',
|
||||
'delete_object',
|
||||
'duplicate_object',
|
||||
'reorder_slides',
|
||||
'create_table',
|
||||
'create_shape',
|
||||
'insert_text',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -348,6 +383,213 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
condition: { field: 'operation', value: 'get_thumbnail' },
|
||||
value: () => 'PNG',
|
||||
},
|
||||
|
||||
// ========== Get Page Operation Fields ==========
|
||||
{
|
||||
id: 'getPageObjectId',
|
||||
title: 'Page/Slide ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the slide/page to retrieve',
|
||||
condition: { field: 'operation', value: 'get_page' },
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ========== Delete Object Operation Fields ==========
|
||||
{
|
||||
id: 'deleteObjectId',
|
||||
title: 'Object ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the element or slide to delete',
|
||||
condition: { field: 'operation', value: 'delete_object' },
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ========== Duplicate Object Operation Fields ==========
|
||||
{
|
||||
id: 'duplicateObjectId',
|
||||
title: 'Object ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the element or slide to duplicate',
|
||||
condition: { field: 'operation', value: 'duplicate_object' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'duplicateObjectIds',
|
||||
title: 'Object ID Mappings',
|
||||
type: 'long-input',
|
||||
placeholder: 'JSON object: {"sourceId1":"newId1","sourceId2":"newId2"}',
|
||||
condition: { field: 'operation', value: 'duplicate_object' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// ========== Reorder Slides Operation Fields ==========
|
||||
{
|
||||
id: 'reorderSlideIds',
|
||||
title: 'Slide IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Comma-separated slide object IDs to move',
|
||||
condition: { field: 'operation', value: 'reorder_slides' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'reorderInsertionIndex',
|
||||
title: 'New Position',
|
||||
type: 'short-input',
|
||||
placeholder: 'Zero-based index where slides should be moved',
|
||||
condition: { field: 'operation', value: 'reorder_slides' },
|
||||
required: true,
|
||||
},
|
||||
|
||||
// ========== Create Table Operation Fields ==========
|
||||
{
|
||||
id: 'tablePageObjectId',
|
||||
title: 'Slide ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the slide to add the table to',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'tableRows',
|
||||
title: 'Rows',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of rows (minimum 1)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'tableColumns',
|
||||
title: 'Columns',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of columns (minimum 1)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'tableWidth',
|
||||
title: 'Width (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Table width in points (default: 400)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
},
|
||||
{
|
||||
id: 'tableHeight',
|
||||
title: 'Height (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Table height in points (default: 200)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
},
|
||||
{
|
||||
id: 'tablePositionX',
|
||||
title: 'X Position (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'X position from left (default: 100)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
},
|
||||
{
|
||||
id: 'tablePositionY',
|
||||
title: 'Y Position (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Y position from top (default: 100)',
|
||||
condition: { field: 'operation', value: 'create_table' },
|
||||
},
|
||||
|
||||
// ========== Create Shape Operation Fields ==========
|
||||
{
|
||||
id: 'shapePageObjectId',
|
||||
title: 'Slide ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the slide to add the shape to',
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'shapeType',
|
||||
title: 'Shape Type',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Text Box', id: 'TEXT_BOX' },
|
||||
{ label: 'Rectangle', id: 'RECTANGLE' },
|
||||
{ label: 'Rounded Rectangle', id: 'ROUND_RECTANGLE' },
|
||||
{ label: 'Ellipse', id: 'ELLIPSE' },
|
||||
{ label: 'Triangle', id: 'TRIANGLE' },
|
||||
{ label: 'Diamond', id: 'DIAMOND' },
|
||||
{ label: 'Star (5 points)', id: 'STAR_5' },
|
||||
{ label: 'Arrow (Right)', id: 'RIGHT_ARROW' },
|
||||
{ label: 'Arrow (Left)', id: 'LEFT_ARROW' },
|
||||
{ label: 'Arrow (Up)', id: 'UP_ARROW' },
|
||||
{ label: 'Arrow (Down)', id: 'DOWN_ARROW' },
|
||||
{ label: 'Heart', id: 'HEART' },
|
||||
{ label: 'Cloud', id: 'CLOUD' },
|
||||
{ label: 'Lightning Bolt', id: 'LIGHTNING_BOLT' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
value: () => 'RECTANGLE',
|
||||
},
|
||||
{
|
||||
id: 'shapeWidth',
|
||||
title: 'Width (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Shape width in points (default: 200)',
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
},
|
||||
{
|
||||
id: 'shapeHeight',
|
||||
title: 'Height (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Shape height in points (default: 100)',
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
},
|
||||
{
|
||||
id: 'shapePositionX',
|
||||
title: 'X Position (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'X position from left (default: 100)',
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
},
|
||||
{
|
||||
id: 'shapePositionY',
|
||||
title: 'Y Position (points)',
|
||||
type: 'short-input',
|
||||
placeholder: 'Y position from top (default: 100)',
|
||||
condition: { field: 'operation', value: 'create_shape' },
|
||||
},
|
||||
|
||||
// ========== Insert Text Operation Fields ==========
|
||||
{
|
||||
id: 'insertTextObjectId',
|
||||
title: 'Object ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Object ID of the shape or table cell',
|
||||
condition: { field: 'operation', value: 'insert_text' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'insertTextContent',
|
||||
title: 'Text',
|
||||
type: 'long-input',
|
||||
placeholder: 'Text to insert',
|
||||
condition: { field: 'operation', value: 'insert_text' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate text content for a presentation slide based on the user's description.
|
||||
The text should be:
|
||||
- Clear and concise
|
||||
- Professional and appropriate for presentations
|
||||
- Well-structured with bullet points if listing items
|
||||
|
||||
Return ONLY the text content - no explanations, no markdown formatting markers, no extra text.`,
|
||||
placeholder: 'Describe the text you want to insert...',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'insertTextIndex',
|
||||
title: 'Insertion Index',
|
||||
type: 'short-input',
|
||||
placeholder: 'Zero-based index (default: 0)',
|
||||
condition: { field: 'operation', value: 'insert_text' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
@@ -358,6 +600,13 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
'google_slides_add_slide',
|
||||
'google_slides_add_image',
|
||||
'google_slides_get_thumbnail',
|
||||
'google_slides_get_page',
|
||||
'google_slides_delete_object',
|
||||
'google_slides_duplicate_object',
|
||||
'google_slides_update_slides_position',
|
||||
'google_slides_create_table',
|
||||
'google_slides_create_shape',
|
||||
'google_slides_insert_text',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -376,6 +625,20 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
return 'google_slides_add_image'
|
||||
case 'get_thumbnail':
|
||||
return 'google_slides_get_thumbnail'
|
||||
case 'get_page':
|
||||
return 'google_slides_get_page'
|
||||
case 'delete_object':
|
||||
return 'google_slides_delete_object'
|
||||
case 'duplicate_object':
|
||||
return 'google_slides_duplicate_object'
|
||||
case 'reorder_slides':
|
||||
return 'google_slides_update_slides_position'
|
||||
case 'create_table':
|
||||
return 'google_slides_create_table'
|
||||
case 'create_shape':
|
||||
return 'google_slides_create_shape'
|
||||
case 'insert_text':
|
||||
return 'google_slides_insert_text'
|
||||
default:
|
||||
throw new Error(`Invalid Google Slides operation: ${params.operation}`)
|
||||
}
|
||||
@@ -439,6 +702,82 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
result.pageObjectId = thumbnailPageId
|
||||
}
|
||||
|
||||
// Get Page operation
|
||||
if (params.operation === 'get_page') {
|
||||
result.pageObjectId = params.getPageObjectId
|
||||
}
|
||||
|
||||
// Delete Object operation
|
||||
if (params.operation === 'delete_object') {
|
||||
result.objectId = params.deleteObjectId
|
||||
}
|
||||
|
||||
// Duplicate Object operation
|
||||
if (params.operation === 'duplicate_object') {
|
||||
result.objectId = params.duplicateObjectId
|
||||
if (params.duplicateObjectIds) {
|
||||
result.objectIds = params.duplicateObjectIds
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder Slides operation
|
||||
if (params.operation === 'reorder_slides') {
|
||||
result.slideObjectIds = params.reorderSlideIds
|
||||
if (params.reorderInsertionIndex) {
|
||||
result.insertionIndex = Number.parseInt(params.reorderInsertionIndex as string, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Table operation
|
||||
if (params.operation === 'create_table') {
|
||||
result.pageObjectId = params.tablePageObjectId
|
||||
if (params.tableRows) {
|
||||
result.rows = Number.parseInt(params.tableRows as string, 10)
|
||||
}
|
||||
if (params.tableColumns) {
|
||||
result.columns = Number.parseInt(params.tableColumns as string, 10)
|
||||
}
|
||||
if (params.tableWidth) {
|
||||
result.width = Number.parseInt(params.tableWidth as string, 10)
|
||||
}
|
||||
if (params.tableHeight) {
|
||||
result.height = Number.parseInt(params.tableHeight as string, 10)
|
||||
}
|
||||
if (params.tablePositionX) {
|
||||
result.positionX = Number.parseInt(params.tablePositionX as string, 10)
|
||||
}
|
||||
if (params.tablePositionY) {
|
||||
result.positionY = Number.parseInt(params.tablePositionY as string, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Create Shape operation
|
||||
if (params.operation === 'create_shape') {
|
||||
result.pageObjectId = params.shapePageObjectId
|
||||
result.shapeType = params.shapeType
|
||||
if (params.shapeWidth) {
|
||||
result.width = Number.parseInt(params.shapeWidth as string, 10)
|
||||
}
|
||||
if (params.shapeHeight) {
|
||||
result.height = Number.parseInt(params.shapeHeight as string, 10)
|
||||
}
|
||||
if (params.shapePositionX) {
|
||||
result.positionX = Number.parseInt(params.shapePositionX as string, 10)
|
||||
}
|
||||
if (params.shapePositionY) {
|
||||
result.positionY = Number.parseInt(params.shapePositionY as string, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert Text operation
|
||||
if (params.operation === 'insert_text') {
|
||||
result.objectId = params.insertTextObjectId
|
||||
result.text = params.insertTextContent
|
||||
if (params.insertTextIndex) {
|
||||
result.insertionIndex = Number.parseInt(params.insertTextIndex as string, 10)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
},
|
||||
@@ -479,6 +818,35 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
thumbnailPageId: { type: 'string', description: 'Slide object ID for thumbnail' },
|
||||
thumbnailSize: { type: 'string', description: 'Thumbnail size' },
|
||||
mimeType: { type: 'string', description: 'Image format (PNG or GIF)' },
|
||||
// Get page operation
|
||||
getPageObjectId: { type: 'string', description: 'Page/slide object ID to retrieve' },
|
||||
// Delete object operation
|
||||
deleteObjectId: { type: 'string', description: 'Object ID to delete' },
|
||||
// Duplicate object operation
|
||||
duplicateObjectId: { type: 'string', description: 'Object ID to duplicate' },
|
||||
duplicateObjectIds: { type: 'string', description: 'JSON object ID mappings' },
|
||||
// Reorder slides operation
|
||||
reorderSlideIds: { type: 'string', description: 'Comma-separated slide IDs to move' },
|
||||
reorderInsertionIndex: { type: 'number', description: 'New position for slides' },
|
||||
// Create table operation
|
||||
tablePageObjectId: { type: 'string', description: 'Slide ID for table' },
|
||||
tableRows: { type: 'number', description: 'Number of rows' },
|
||||
tableColumns: { type: 'number', description: 'Number of columns' },
|
||||
tableWidth: { type: 'number', description: 'Table width in points' },
|
||||
tableHeight: { type: 'number', description: 'Table height in points' },
|
||||
tablePositionX: { type: 'number', description: 'Table X position in points' },
|
||||
tablePositionY: { type: 'number', description: 'Table Y position in points' },
|
||||
// Create shape operation
|
||||
shapePageObjectId: { type: 'string', description: 'Slide ID for shape' },
|
||||
shapeType: { type: 'string', description: 'Shape type' },
|
||||
shapeWidth: { type: 'number', description: 'Shape width in points' },
|
||||
shapeHeight: { type: 'number', description: 'Shape height in points' },
|
||||
shapePositionX: { type: 'number', description: 'Shape X position in points' },
|
||||
shapePositionY: { type: 'number', description: 'Shape Y position in points' },
|
||||
// Insert text operation
|
||||
insertTextObjectId: { type: 'string', description: 'Object ID for text insertion' },
|
||||
insertTextContent: { type: 'string', description: 'Text to insert' },
|
||||
insertTextIndex: { type: 'number', description: 'Insertion index' },
|
||||
},
|
||||
outputs: {
|
||||
// Read operation
|
||||
@@ -496,5 +864,26 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
contentUrl: { type: 'string', description: 'URL to the thumbnail image' },
|
||||
width: { type: 'number', description: 'Thumbnail width in pixels' },
|
||||
height: { type: 'number', description: 'Thumbnail height in pixels' },
|
||||
// Get page operation
|
||||
objectId: { type: 'string', description: 'Page object ID' },
|
||||
pageType: { type: 'string', description: 'Page type (SLIDE, MASTER, etc.)' },
|
||||
pageElements: { type: 'json', description: 'Page elements array' },
|
||||
slideProperties: { type: 'json', description: 'Slide-specific properties' },
|
||||
// Delete object operation
|
||||
deleted: { type: 'boolean', description: 'Whether object was deleted' },
|
||||
// Duplicate object operation
|
||||
duplicatedObjectId: { type: 'string', description: 'Object ID of the duplicate' },
|
||||
// Reorder slides operation
|
||||
moved: { type: 'boolean', description: 'Whether slides were moved' },
|
||||
slideObjectIds: { type: 'json', description: 'Slide IDs that were moved' },
|
||||
// Create table operation
|
||||
tableId: { type: 'string', description: 'Object ID of newly created table' },
|
||||
rows: { type: 'number', description: 'Number of rows created' },
|
||||
columns: { type: 'number', description: 'Number of columns created' },
|
||||
// Create shape operation
|
||||
shapeId: { type: 'string', description: 'Object ID of newly created shape' },
|
||||
// Insert text operation
|
||||
inserted: { type: 'boolean', description: 'Whether text was inserted' },
|
||||
text: { type: 'string', description: 'Text that was inserted' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -535,6 +535,51 @@ Return ONLY the search query - no explanations.`,
|
||||
value: ['linear_read_issues', 'linear_search_issues', 'linear_list_projects'],
|
||||
},
|
||||
},
|
||||
// Issue filtering options for read_issues (advanced)
|
||||
{
|
||||
id: 'labelIds',
|
||||
title: 'Label IDs',
|
||||
type: 'short-input',
|
||||
placeholder: 'Array of label IDs to filter by',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'createdAfter',
|
||||
title: 'Created After',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter issues created after this date (ISO 8601 format)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updatedAfter',
|
||||
title: 'Updated After',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter issues updated after this date (ISO 8601 format)',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'orderBy',
|
||||
title: 'Order By',
|
||||
type: 'short-input',
|
||||
placeholder: 'Sort order: "createdAt" or "updatedAt" (default: "updatedAt")',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'linear_read_issues',
|
||||
},
|
||||
},
|
||||
// Cycle ID
|
||||
{
|
||||
id: 'cycleId',
|
||||
@@ -2188,6 +2233,16 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
estimate: { type: 'string', description: 'Estimate points' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
includeArchived: { type: 'boolean', description: 'Include archived items' },
|
||||
labelIds: { type: 'array', description: 'Array of label IDs to filter by' },
|
||||
createdAfter: {
|
||||
type: 'string',
|
||||
description: 'Filter issues created after this date (ISO 8601)',
|
||||
},
|
||||
updatedAfter: {
|
||||
type: 'string',
|
||||
description: 'Filter issues updated after this date (ISO 8601)',
|
||||
},
|
||||
orderBy: { type: 'string', description: 'Sort order: createdAt or updatedAt' },
|
||||
cycleId: { type: 'string', description: 'Cycle identifier' },
|
||||
startDate: { type: 'string', description: 'Start date' },
|
||||
endDate: { type: 'string', description: 'End date' },
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { MicrosoftExcelIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftExcelResponse } from '@/tools/microsoft_excel/types'
|
||||
import type {
|
||||
MicrosoftExcelResponse,
|
||||
MicrosoftExcelV2Response,
|
||||
} from '@/tools/microsoft_excel/types'
|
||||
|
||||
export const MicrosoftExcelBlock: BlockConfig<MicrosoftExcelResponse> = {
|
||||
type: 'microsoft_excel',
|
||||
@@ -325,3 +328,260 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
|
||||
type: 'microsoft_excel_v2',
|
||||
name: 'Microsoft Excel',
|
||||
description: 'Read and write data with sheet selection',
|
||||
authMode: AuthMode.OAuth,
|
||||
hideFromToolbar: false,
|
||||
longDescription:
|
||||
'Integrate Microsoft Excel into the workflow with explicit sheet selection. Can read and write data in specific sheets.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_excel',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: MicrosoftExcelIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Read Data', id: 'read' },
|
||||
{ label: 'Write Data', id: 'write' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Microsoft Excel Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
// Spreadsheet Selector (basic mode)
|
||||
{
|
||||
id: 'spreadsheetId',
|
||||
title: 'Select Spreadsheet',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [],
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Spreadsheet ID (advanced mode)
|
||||
{
|
||||
id: 'manualSpreadsheetId',
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'Enter spreadsheet ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Sheet Name Selector (basic mode)
|
||||
{
|
||||
id: 'sheetName',
|
||||
title: 'Sheet (Tab)',
|
||||
type: 'sheet-selector',
|
||||
canonicalParamId: 'sheetName',
|
||||
serviceId: 'microsoft-excel',
|
||||
placeholder: 'Select a sheet',
|
||||
required: true,
|
||||
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Sheet Name (advanced mode)
|
||||
{
|
||||
id: 'manualSheetName',
|
||||
title: 'Sheet Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'sheetName',
|
||||
placeholder: 'Name of the sheet/tab (e.g., Sheet1)',
|
||||
required: true,
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Cell Range (optional for read/write)
|
||||
{
|
||||
id: 'cellRange',
|
||||
title: 'Cell Range',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cell range (e.g., A1:D10). Defaults to used range for read, A1 for write.',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a valid cell range based on the user's description.
|
||||
|
||||
### VALID FORMATS
|
||||
- Single cell: A1
|
||||
- Range: A1:D10
|
||||
- Entire column: A:A
|
||||
- Entire row: 1:1
|
||||
- Multiple columns: A:D
|
||||
- Multiple rows: 1:10
|
||||
|
||||
### RANGE RULES
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc.
|
||||
- Row numbers start at 1 (not 0)
|
||||
|
||||
### EXAMPLES
|
||||
- "first 100 rows" -> A1:Z100
|
||||
- "cells A1 through C50" -> A1:C50
|
||||
- "column A" -> A:A
|
||||
- "just the headers row" -> 1:1
|
||||
- "first cell" -> A1
|
||||
|
||||
Return ONLY the range string - no sheet name, no explanations, no quotes.`,
|
||||
placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...',
|
||||
},
|
||||
},
|
||||
// Write-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Microsoft Excel data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]]
|
||||
- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to write...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['microsoft_excel_read_v2', 'microsoft_excel_write_v2'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read':
|
||||
return 'microsoft_excel_read_v2'
|
||||
case 'write':
|
||||
return 'microsoft_excel_write_v2'
|
||||
default:
|
||||
throw new Error(`Invalid Microsoft Excel V2 operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
values,
|
||||
spreadsheetId,
|
||||
manualSpreadsheetId,
|
||||
sheetName,
|
||||
manualSheetName,
|
||||
cellRange,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
if (!effectiveSheetName) {
|
||||
throw new Error('Sheet name is required. Please select or enter a sheet name.')
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
sheetName: effectiveSheetName,
|
||||
cellRange: cellRange ? (cellRange as string).trim() : undefined,
|
||||
values: parsedValues,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Microsoft Excel access token' },
|
||||
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
|
||||
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
|
||||
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
|
||||
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
|
||||
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
|
||||
values: { type: 'string', description: 'Cell values data' },
|
||||
valueInputOption: { type: 'string', description: 'Value input option' },
|
||||
},
|
||||
outputs: {
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
description: 'Name of the sheet',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
description: 'Range that was read',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description: 'Cell values as 2D array',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
updatedRange: {
|
||||
type: 'string',
|
||||
description: 'Updated range',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedRows: {
|
||||
type: 'number',
|
||||
description: 'Updated rows count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Updated columns count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedCells: {
|
||||
type: 'number',
|
||||
description: 'Updated cells count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
import { MicrosoftExcelIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { MicrosoftExcelV2Response } from '@/tools/microsoft_excel/types'
|
||||
|
||||
export const MicrosoftExcelV2Block: BlockConfig<MicrosoftExcelV2Response> = {
|
||||
type: 'microsoft_excel_v2',
|
||||
name: 'Microsoft Excel',
|
||||
description: 'Read and write data with sheet selection',
|
||||
authMode: AuthMode.OAuth,
|
||||
hideFromToolbar: false,
|
||||
longDescription:
|
||||
'Integrate Microsoft Excel into the workflow with explicit sheet selection. Can read and write data in specific sheets.',
|
||||
docsLink: 'https://docs.sim.ai/tools/microsoft_excel',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: MicrosoftExcelIcon,
|
||||
subBlocks: [
|
||||
// Operation selector
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Read Data', id: 'read' },
|
||||
{ label: 'Write Data', id: 'write' },
|
||||
],
|
||||
value: () => 'read',
|
||||
},
|
||||
// Microsoft Excel Credentials
|
||||
{
|
||||
id: 'credential',
|
||||
title: 'Microsoft Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [
|
||||
'openid',
|
||||
'profile',
|
||||
'email',
|
||||
'Files.Read',
|
||||
'Files.ReadWrite',
|
||||
'offline_access',
|
||||
],
|
||||
placeholder: 'Select Microsoft account',
|
||||
required: true,
|
||||
},
|
||||
// Spreadsheet Selector (basic mode)
|
||||
{
|
||||
id: 'spreadsheetId',
|
||||
title: 'Select Spreadsheet',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
serviceId: 'microsoft-excel',
|
||||
requiredScopes: [],
|
||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
placeholder: 'Select a spreadsheet',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Spreadsheet ID (advanced mode)
|
||||
{
|
||||
id: 'manualSpreadsheetId',
|
||||
title: 'Spreadsheet ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'spreadsheetId',
|
||||
placeholder: 'Enter spreadsheet ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Sheet Name Selector (basic mode)
|
||||
{
|
||||
id: 'sheetName',
|
||||
title: 'Sheet (Tab)',
|
||||
type: 'sheet-selector',
|
||||
canonicalParamId: 'sheetName',
|
||||
serviceId: 'microsoft-excel',
|
||||
placeholder: 'Select a sheet',
|
||||
required: true,
|
||||
dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] },
|
||||
mode: 'basic',
|
||||
},
|
||||
// Manual Sheet Name (advanced mode)
|
||||
{
|
||||
id: 'manualSheetName',
|
||||
title: 'Sheet Name',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'sheetName',
|
||||
placeholder: 'Name of the sheet/tab (e.g., Sheet1)',
|
||||
required: true,
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
},
|
||||
// Cell Range (optional for read/write)
|
||||
{
|
||||
id: 'cellRange',
|
||||
title: 'Cell Range',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cell range (e.g., A1:D10). Defaults to used range for read, A1 for write.',
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate a valid cell range based on the user's description.
|
||||
|
||||
### VALID FORMATS
|
||||
- Single cell: A1
|
||||
- Range: A1:D10
|
||||
- Entire column: A:A
|
||||
- Entire row: 1:1
|
||||
- Multiple columns: A:D
|
||||
- Multiple rows: 1:10
|
||||
|
||||
### RANGE RULES
|
||||
- Column letters are uppercase: A, B, C, ... Z, AA, AB, etc.
|
||||
- Row numbers start at 1 (not 0)
|
||||
|
||||
### EXAMPLES
|
||||
- "first 100 rows" -> A1:Z100
|
||||
- "cells A1 through C50" -> A1:C50
|
||||
- "column A" -> A:A
|
||||
- "just the headers row" -> 1:1
|
||||
- "first cell" -> A1
|
||||
|
||||
Return ONLY the range string - no sheet name, no explanations, no quotes.`,
|
||||
placeholder: 'Describe the range (e.g., "first 50 rows" or "column A")...',
|
||||
},
|
||||
},
|
||||
// Write-specific Fields
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'Enter values as JSON array of arrays (e.g., [["A1", "B1"], ["A2", "B2"]]) or an array of objects (e.g., [{"name":"John", "age":30}])',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
prompt: `Generate Microsoft Excel data as a JSON array based on the user's description.
|
||||
|
||||
Format options:
|
||||
1. Array of arrays: [["Header1", "Header2"], ["Value1", "Value2"]]
|
||||
2. Array of objects: [{"column1": "value1", "column2": "value2"}]
|
||||
|
||||
Examples:
|
||||
- "sales data with product and revenue columns" -> [["Product", "Revenue"], ["Widget A", 1500], ["Widget B", 2300]]
|
||||
- "list of employees with name and email" -> [{"name": "John Doe", "email": "john@example.com"}, {"name": "Jane Smith", "email": "jane@example.com"}]
|
||||
|
||||
Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the data you want to write...',
|
||||
generationType: 'json-object',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'valueInputOption',
|
||||
title: 'Value Input Option',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
|
||||
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
|
||||
],
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['microsoft_excel_read_v2', 'microsoft_excel_write_v2'],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'read':
|
||||
return 'microsoft_excel_read_v2'
|
||||
case 'write':
|
||||
return 'microsoft_excel_write_v2'
|
||||
default:
|
||||
throw new Error(`Invalid Microsoft Excel V2 operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const {
|
||||
credential,
|
||||
values,
|
||||
spreadsheetId,
|
||||
manualSpreadsheetId,
|
||||
sheetName,
|
||||
manualSheetName,
|
||||
cellRange,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const parsedValues = values ? JSON.parse(values as string) : undefined
|
||||
|
||||
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
|
||||
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
|
||||
|
||||
if (!effectiveSpreadsheetId) {
|
||||
throw new Error('Spreadsheet ID is required.')
|
||||
}
|
||||
|
||||
if (!effectiveSheetName) {
|
||||
throw new Error('Sheet name is required. Please select or enter a sheet name.')
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
spreadsheetId: effectiveSpreadsheetId,
|
||||
sheetName: effectiveSheetName,
|
||||
cellRange: cellRange ? (cellRange as string).trim() : undefined,
|
||||
values: parsedValues,
|
||||
credential,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Microsoft Excel access token' },
|
||||
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
|
||||
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
|
||||
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
|
||||
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
|
||||
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
|
||||
values: { type: 'string', description: 'Cell values data' },
|
||||
valueInputOption: { type: 'string', description: 'Value input option' },
|
||||
},
|
||||
outputs: {
|
||||
sheetName: {
|
||||
type: 'string',
|
||||
description: 'Name of the sheet',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
range: {
|
||||
type: 'string',
|
||||
description: 'Range that was read',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description: 'Cell values as 2D array',
|
||||
condition: { field: 'operation', value: 'read' },
|
||||
},
|
||||
updatedRange: {
|
||||
type: 'string',
|
||||
description: 'Updated range',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedRows: {
|
||||
type: 'number',
|
||||
description: 'Updated rows count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedColumns: {
|
||||
type: 'number',
|
||||
description: 'Updated columns count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
updatedCells: {
|
||||
type: 'number',
|
||||
description: 'Updated cells count',
|
||||
condition: { field: 'operation', value: 'write' },
|
||||
},
|
||||
metadata: { type: 'json', description: 'Spreadsheet metadata including ID and URL' },
|
||||
},
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { ApifyBlock } from '@/blocks/blocks/apify'
|
||||
import { ApolloBlock } from '@/blocks/blocks/apollo'
|
||||
import { ArxivBlock } from '@/blocks/blocks/arxiv'
|
||||
import { AsanaBlock } from '@/blocks/blocks/asana'
|
||||
// import { BoxBlock } from '@/blocks/blocks/box' // TODO: Box OAuth integration
|
||||
import { BrowserUseBlock } from '@/blocks/blocks/browser_use'
|
||||
import { CalendlyBlock } from '@/blocks/blocks/calendly'
|
||||
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||
@@ -38,10 +37,9 @@ import { GoogleSearchBlock } from '@/blocks/blocks/google'
|
||||
import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar'
|
||||
import { GoogleDocsBlock } from '@/blocks/blocks/google_docs'
|
||||
import { GoogleDriveBlock } from '@/blocks/blocks/google_drive'
|
||||
import { GoogleFormsBlock } from '@/blocks/blocks/google_form'
|
||||
import { GoogleFormsBlock } from '@/blocks/blocks/google_forms'
|
||||
import { GoogleGroupsBlock } from '@/blocks/blocks/google_groups'
|
||||
import { GoogleSheetsBlock } from '@/blocks/blocks/google_sheets'
|
||||
import { GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets_v2'
|
||||
import { GoogleSheetsBlock, GoogleSheetsV2Block } from '@/blocks/blocks/google_sheets'
|
||||
import { GoogleSlidesBlock } from '@/blocks/blocks/google_slides'
|
||||
import { GoogleVaultBlock } from '@/blocks/blocks/google_vault'
|
||||
import { GrafanaBlock } from '@/blocks/blocks/grafana'
|
||||
@@ -73,8 +71,7 @@ import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
|
||||
import { McpBlock } from '@/blocks/blocks/mcp'
|
||||
import { Mem0Block } from '@/blocks/blocks/mem0'
|
||||
import { MemoryBlock } from '@/blocks/blocks/memory'
|
||||
import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel'
|
||||
import { MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel_v2'
|
||||
import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel'
|
||||
import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner'
|
||||
import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams'
|
||||
import { MistralParseBlock } from '@/blocks/blocks/mistral_parse'
|
||||
@@ -116,6 +113,7 @@ import { ShopifyBlock } from '@/blocks/blocks/shopify'
|
||||
import { SlackBlock } from '@/blocks/blocks/slack'
|
||||
import { SmtpBlock } from '@/blocks/blocks/smtp'
|
||||
import { SpotifyBlock } from '@/blocks/blocks/spotify'
|
||||
import { SQSBlock } from '@/blocks/blocks/sqs'
|
||||
import { SSHBlock } from '@/blocks/blocks/ssh'
|
||||
import { StagehandBlock } from '@/blocks/blocks/stagehand'
|
||||
import { StartTriggerBlock } from '@/blocks/blocks/start_trigger'
|
||||
@@ -151,7 +149,6 @@ import { ZendeskBlock } from '@/blocks/blocks/zendesk'
|
||||
import { ZepBlock } from '@/blocks/blocks/zep'
|
||||
import { ZoomBlock } from '@/blocks/blocks/zoom'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { SQSBlock } from './blocks/sqs'
|
||||
|
||||
// Registry of all available blocks, alphabetically sorted
|
||||
export const registry: Record<string, BlockConfig> = {
|
||||
@@ -165,7 +162,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
apollo: ApolloBlock,
|
||||
arxiv: ArxivBlock,
|
||||
asana: AsanaBlock,
|
||||
// box: BoxBlock, // TODO: Box OAuth integration
|
||||
browser_use: BrowserUseBlock,
|
||||
calendly: CalendlyBlock,
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
@@ -179,8 +175,9 @@ export const registry: Record<string, BlockConfig> = {
|
||||
discord: DiscordBlock,
|
||||
dropbox: DropboxBlock,
|
||||
duckduckgo: DuckDuckGoBlock,
|
||||
elevenlabs: ElevenLabsBlock,
|
||||
dynamodb: DynamoDBBlock,
|
||||
elasticsearch: ElasticsearchBlock,
|
||||
elevenlabs: ElevenLabsBlock,
|
||||
evaluator: EvaluatorBlock,
|
||||
exa: ExaBlock,
|
||||
file: FileBlock,
|
||||
@@ -193,21 +190,21 @@ export const registry: Record<string, BlockConfig> = {
|
||||
gitlab: GitLabBlock,
|
||||
gmail: GmailBlock,
|
||||
gmail_v2: GmailV2Block,
|
||||
grain: GrainBlock,
|
||||
grafana: GrafanaBlock,
|
||||
greptile: GreptileBlock,
|
||||
guardrails: GuardrailsBlock,
|
||||
google_calendar: GoogleCalendarBlock,
|
||||
google_calendar_v2: GoogleCalendarV2Block,
|
||||
google_docs: GoogleDocsBlock,
|
||||
google_drive: GoogleDriveBlock,
|
||||
google_forms: GoogleFormsBlock,
|
||||
google_groups: GoogleGroupsBlock,
|
||||
google_search: GoogleSearchBlock,
|
||||
google_sheets: GoogleSheetsBlock,
|
||||
google_sheets_v2: GoogleSheetsV2Block,
|
||||
google_slides: GoogleSlidesBlock,
|
||||
google_vault: GoogleVaultBlock,
|
||||
google_groups: GoogleGroupsBlock,
|
||||
grafana: GrafanaBlock,
|
||||
grain: GrainBlock,
|
||||
greptile: GreptileBlock,
|
||||
guardrails: GuardrailsBlock,
|
||||
hubspot: HubSpotBlock,
|
||||
huggingface: HuggingFaceBlock,
|
||||
human_in_the_loop: HumanInTheLoopBlock,
|
||||
@@ -239,7 +236,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
microsoft_planner: MicrosoftPlannerBlock,
|
||||
microsoft_teams: MicrosoftTeamsBlock,
|
||||
mistral_parse: MistralParseBlock,
|
||||
reducto: ReductoBlock,
|
||||
mongodb: MongoDBBlock,
|
||||
mysql: MySQLBlock,
|
||||
neo4j: Neo4jBlock,
|
||||
@@ -259,35 +255,34 @@ export const registry: Record<string, BlockConfig> = {
|
||||
pulse: PulseBlock,
|
||||
qdrant: QdrantBlock,
|
||||
rds: RDSBlock,
|
||||
sqs: SQSBlock,
|
||||
dynamodb: DynamoDBBlock,
|
||||
reddit: RedditBlock,
|
||||
reducto: ReductoBlock,
|
||||
resend: ResendBlock,
|
||||
response: ResponseBlock,
|
||||
rss: RssBlock,
|
||||
router: RouterBlock,
|
||||
router_v2: RouterV2Block,
|
||||
rss: RssBlock,
|
||||
s3: S3Block,
|
||||
salesforce: SalesforceBlock,
|
||||
schedule: ScheduleBlock,
|
||||
search: SearchBlock,
|
||||
sendgrid: SendGridBlock,
|
||||
sentry: SentryBlock,
|
||||
servicenow: ServiceNowBlock,
|
||||
serper: SerperBlock,
|
||||
servicenow: ServiceNowBlock,
|
||||
sftp: SftpBlock,
|
||||
sharepoint: SharepointBlock,
|
||||
shopify: ShopifyBlock,
|
||||
slack: SlackBlock,
|
||||
spotify: SpotifyBlock,
|
||||
smtp: SmtpBlock,
|
||||
sftp: SftpBlock,
|
||||
spotify: SpotifyBlock,
|
||||
sqs: SQSBlock,
|
||||
ssh: SSHBlock,
|
||||
stagehand: StagehandBlock,
|
||||
starter: StarterBlock,
|
||||
start_trigger: StartTriggerBlock,
|
||||
stt: SttBlock,
|
||||
tts: TtsBlock,
|
||||
starter: StarterBlock,
|
||||
stripe: StripeBlock,
|
||||
stt: SttBlock,
|
||||
supabase: SupabaseBlock,
|
||||
tavily: TavilyBlock,
|
||||
telegram: TelegramBlock,
|
||||
@@ -295,6 +290,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
tinybird: TinybirdBlock,
|
||||
translate: TranslateBlock,
|
||||
trello: TrelloBlock,
|
||||
tts: TtsBlock,
|
||||
twilio_sms: TwilioSMSBlock,
|
||||
twilio_voice: TwilioVoiceBlock,
|
||||
typeform: TypeformBlock,
|
||||
@@ -312,8 +308,8 @@ export const registry: Record<string, BlockConfig> = {
|
||||
workflow_input: WorkflowInputBlock,
|
||||
x: XBlock,
|
||||
youtube: YouTubeBlock,
|
||||
zep: ZepBlock,
|
||||
zendesk: ZendeskBlock,
|
||||
zep: ZepBlock,
|
||||
zoom: ZoomBlock,
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -200,7 +200,7 @@ const ModalHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<DialogPrimitive.Title className='min-w-0 truncate font-medium text-[16px] text-[var(--text-primary)]'>
|
||||
<DialogPrimitive.Title className='min-w-0 font-medium text-[16px] text-[var(--text-primary)] leading-snug'>
|
||||
{children}
|
||||
</DialogPrimitive.Title>
|
||||
<DialogPrimitive.Close asChild>
|
||||
|
||||
@@ -321,6 +321,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -1074,6 +1075,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
})
|
||||
|
||||
return this.processProviderResponse(response, block, responseFormat)
|
||||
|
||||
@@ -78,6 +78,7 @@ export class ApiBlockHandler implements BlockHandler {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
executionId: ctx.executionId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -40,6 +40,7 @@ export async function evaluateConditionExpression(
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -38,6 +38,7 @@ export class FunctionBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -66,6 +66,7 @@ export class GenericBlockHandler implements BlockHandler {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
executionId: ctx.executionId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -627,6 +627,7 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
},
|
||||
blockData: blockDataWithPause,
|
||||
blockNameMapping: blockNameMappingWithPause,
|
||||
|
||||
@@ -6,10 +6,14 @@ import type { ResolutionContext } from './reference'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
/**
|
||||
* Creates a minimal workflow for testing.
|
||||
*/
|
||||
function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: string }> = []) {
|
||||
function createTestWorkflow(
|
||||
blocks: Array<{
|
||||
id: string
|
||||
name?: string
|
||||
type?: string
|
||||
outputs?: Record<string, any>
|
||||
}> = []
|
||||
) {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: blocks.map((b) => ({
|
||||
@@ -17,7 +21,7 @@ function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: st
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: b.type ?? 'function', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
outputs: b.outputs ?? {},
|
||||
metadata: { id: b.type ?? 'function', name: b.name ?? b.id },
|
||||
enabled: true,
|
||||
})),
|
||||
@@ -126,7 +130,7 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.items.1.id>', ctx)).toBe(2)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent path', () => {
|
||||
it.concurrent('should return undefined for non-existent path when no schema defined', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
@@ -136,6 +140,48 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.nonexistent>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should throw error for path not in output schema', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
validField: { type: 'string', description: 'A valid field' },
|
||||
nested: {
|
||||
child: { type: 'number', description: 'Nested child' },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { validField: 'value', nested: { child: 42 } },
|
||||
})
|
||||
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(
|
||||
/"invalidField" doesn't exist on block "source"/
|
||||
)
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(/Available fields:/)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for path in schema but missing in data', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
requiredField: { type: 'string', description: 'Always present' },
|
||||
optionalField: { type: 'string', description: 'Sometimes missing' },
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { requiredField: 'value' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.requiredField>', ctx)).toBe('value')
|
||||
expect(resolver.resolve('<source.optionalField>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent block', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'existing' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
|
||||
@@ -9,14 +9,75 @@ import {
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
function isPathInOutputSchema(
|
||||
outputs: Record<string, any> | undefined,
|
||||
pathParts: string[]
|
||||
): boolean {
|
||||
if (!outputs || pathParts.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
let current: any = outputs
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const part = pathParts[i]
|
||||
|
||||
if (/^\d+$/.test(part)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (part in current) {
|
||||
current = current[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.properties && part in current.properties) {
|
||||
current = current.properties[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.type === 'array' && current.items) {
|
||||
if (current.items.properties && part in current.items.properties) {
|
||||
current = current.items.properties[part]
|
||||
continue
|
||||
}
|
||||
if (part in current.items) {
|
||||
current = current.items[part]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ('type' in current && typeof current.type === 'string') {
|
||||
if (!current.properties && !current.items) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getSchemaFieldNames(outputs: Record<string, any> | undefined): string[] {
|
||||
if (!outputs) return []
|
||||
return Object.keys(outputs)
|
||||
}
|
||||
|
||||
export class BlockResolver implements Resolver {
|
||||
private nameToBlockId: Map<string, string>
|
||||
private blockById: Map<string, SerializedBlock>
|
||||
|
||||
constructor(private workflow: SerializedWorkflow) {
|
||||
this.nameToBlockId = new Map()
|
||||
this.blockById = new Map()
|
||||
for (const block of workflow.blocks) {
|
||||
this.blockById.set(block.id, block)
|
||||
if (block.metadata?.name) {
|
||||
this.nameToBlockId.set(normalizeName(block.metadata.name), block.id)
|
||||
}
|
||||
@@ -47,7 +108,9 @@ export class BlockResolver implements Resolver {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const block = this.blockById.get(blockId)
|
||||
const output = this.getBlockOutput(blockId, context)
|
||||
|
||||
if (output === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@@ -63,9 +126,6 @@ export class BlockResolver implements Resolver {
|
||||
return result
|
||||
}
|
||||
|
||||
// If failed, check if we should try backwards compatibility fallback
|
||||
const block = this.workflow.blocks.find((b) => b.id === blockId)
|
||||
|
||||
// Response block backwards compatibility:
|
||||
// Old: <responseBlock.response.data> -> New: <responseBlock.data>
|
||||
// Only apply fallback if:
|
||||
@@ -108,6 +168,14 @@ export class BlockResolver implements Resolver {
|
||||
}
|
||||
}
|
||||
|
||||
const schemaFields = getSchemaFieldNames(block?.outputs)
|
||||
if (schemaFields.length > 0 && !isPathInOutputSchema(block?.outputs, pathParts)) {
|
||||
throw new Error(
|
||||
`"${pathParts.join('.')}" doesn't exist on block "${blockName}". ` +
|
||||
`Available fields: ${schemaFields.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@ export function useWorkflowInputFields(workflowId: string | undefined) {
|
||||
queryKey: workflowKeys.inputFields(workflowId),
|
||||
queryFn: () => fetchWorkflowInputFields(workflowId!),
|
||||
enabled: Boolean(workflowId),
|
||||
staleTime: 60 * 1000, // 1 minute cache
|
||||
staleTime: 0,
|
||||
refetchOnMount: 'always',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { Edge } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { useSocket } from '@/app/workspace/providers/socket-provider'
|
||||
import { getBlock } from '@/blocks'
|
||||
@@ -128,12 +129,14 @@ export function useCollaborativeWorkflow() {
|
||||
onOperationFailed,
|
||||
} = useSocket()
|
||||
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const subBlockStore = useSubBlockStore()
|
||||
const variablesStore = useVariablesStore()
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
const { data: session } = useSession()
|
||||
const { hasActiveDiff, isShowingDiff } = useWorkflowDiffStore()
|
||||
const { hasActiveDiff, isShowingDiff } = useWorkflowDiffStore(
|
||||
useShallow((state) => ({
|
||||
hasActiveDiff: state.hasActiveDiff,
|
||||
isShowingDiff: state.isShowingDiff,
|
||||
}))
|
||||
)
|
||||
const isBaselineDiffView = hasActiveDiff && !isShowingDiff
|
||||
|
||||
// Track if we're applying remote changes to avoid infinite loops
|
||||
@@ -198,17 +201,15 @@ export function useCollaborativeWorkflow() {
|
||||
if (target === OPERATION_TARGETS.BLOCK) {
|
||||
switch (operation) {
|
||||
case BLOCK_OPERATIONS.UPDATE_NAME:
|
||||
workflowStore.updateBlockName(payload.id, payload.name)
|
||||
useWorkflowStore.getState().updateBlockName(payload.id, payload.name)
|
||||
break
|
||||
case BLOCK_OPERATIONS.UPDATE_ADVANCED_MODE:
|
||||
workflowStore.setBlockAdvancedMode(payload.id, payload.advancedMode)
|
||||
useWorkflowStore.getState().setBlockAdvancedMode(payload.id, payload.advancedMode)
|
||||
break
|
||||
case BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE:
|
||||
workflowStore.setBlockCanonicalMode(
|
||||
payload.id,
|
||||
payload.canonicalId,
|
||||
payload.canonicalMode
|
||||
)
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.setBlockCanonicalMode(payload.id, payload.canonicalId, payload.canonicalMode)
|
||||
break
|
||||
}
|
||||
} else if (target === OPERATION_TARGETS.BLOCKS) {
|
||||
@@ -216,7 +217,7 @@ export function useCollaborativeWorkflow() {
|
||||
case BLOCKS_OPERATIONS.BATCH_UPDATE_POSITIONS: {
|
||||
const { updates } = payload
|
||||
if (Array.isArray(updates)) {
|
||||
workflowStore.batchUpdatePositions(updates)
|
||||
useWorkflowStore.getState().batchUpdatePositions(updates)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -226,7 +227,7 @@ export function useCollaborativeWorkflow() {
|
||||
case EDGES_OPERATIONS.BATCH_REMOVE_EDGES: {
|
||||
const { ids } = payload
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
workflowStore.batchRemoveEdges(ids)
|
||||
useWorkflowStore.getState().batchRemoveEdges(ids)
|
||||
|
||||
const updatedBlocks = useWorkflowStore.getState().blocks
|
||||
const updatedEdges = useWorkflowStore.getState().edges
|
||||
@@ -249,9 +250,9 @@ export function useCollaborativeWorkflow() {
|
||||
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
||||
const { edges } = payload
|
||||
if (Array.isArray(edges) && edges.length > 0) {
|
||||
const newEdges = filterNewEdges(edges, workflowStore.edges)
|
||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||
if (newEdges.length > 0) {
|
||||
workflowStore.batchAddEdges(newEdges)
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -264,30 +265,36 @@ export function useCollaborativeWorkflow() {
|
||||
if (payload.type === 'loop') {
|
||||
const { config } = payload
|
||||
if (config.loopType !== undefined) {
|
||||
workflowStore.updateLoopType(payload.id, config.loopType)
|
||||
useWorkflowStore.getState().updateLoopType(payload.id, config.loopType)
|
||||
}
|
||||
if (config.iterations !== undefined) {
|
||||
workflowStore.updateLoopCount(payload.id, config.iterations)
|
||||
useWorkflowStore.getState().updateLoopCount(payload.id, config.iterations)
|
||||
}
|
||||
if (config.forEachItems !== undefined) {
|
||||
workflowStore.setLoopForEachItems(payload.id, config.forEachItems)
|
||||
useWorkflowStore.getState().setLoopForEachItems(payload.id, config.forEachItems)
|
||||
}
|
||||
if (config.whileCondition !== undefined) {
|
||||
workflowStore.setLoopWhileCondition(payload.id, config.whileCondition)
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.setLoopWhileCondition(payload.id, config.whileCondition)
|
||||
}
|
||||
if (config.doWhileCondition !== undefined) {
|
||||
workflowStore.setLoopDoWhileCondition(payload.id, config.doWhileCondition)
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.setLoopDoWhileCondition(payload.id, config.doWhileCondition)
|
||||
}
|
||||
} else if (payload.type === 'parallel') {
|
||||
const { config } = payload
|
||||
if (config.parallelType !== undefined) {
|
||||
workflowStore.updateParallelType(payload.id, config.parallelType)
|
||||
useWorkflowStore.getState().updateParallelType(payload.id, config.parallelType)
|
||||
}
|
||||
if (config.count !== undefined) {
|
||||
workflowStore.updateParallelCount(payload.id, config.count)
|
||||
useWorkflowStore.getState().updateParallelCount(payload.id, config.count)
|
||||
}
|
||||
if (config.distribution !== undefined) {
|
||||
workflowStore.updateParallelCollection(payload.id, config.distribution)
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.updateParallelCollection(payload.id, config.distribution)
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -295,7 +302,7 @@ export function useCollaborativeWorkflow() {
|
||||
} else if (target === OPERATION_TARGETS.VARIABLE) {
|
||||
switch (operation) {
|
||||
case VARIABLE_OPERATIONS.ADD:
|
||||
variablesStore.addVariable(
|
||||
useVariablesStore.getState().addVariable(
|
||||
{
|
||||
workflowId: payload.workflowId,
|
||||
name: payload.name,
|
||||
@@ -307,15 +314,21 @@ export function useCollaborativeWorkflow() {
|
||||
break
|
||||
case VARIABLE_OPERATIONS.UPDATE:
|
||||
if (payload.field === 'name') {
|
||||
variablesStore.updateVariable(payload.variableId, { name: payload.value })
|
||||
useVariablesStore
|
||||
.getState()
|
||||
.updateVariable(payload.variableId, { name: payload.value })
|
||||
} else if (payload.field === 'value') {
|
||||
variablesStore.updateVariable(payload.variableId, { value: payload.value })
|
||||
useVariablesStore
|
||||
.getState()
|
||||
.updateVariable(payload.variableId, { value: payload.value })
|
||||
} else if (payload.field === 'type') {
|
||||
variablesStore.updateVariable(payload.variableId, { type: payload.value })
|
||||
useVariablesStore
|
||||
.getState()
|
||||
.updateVariable(payload.variableId, { type: payload.value })
|
||||
}
|
||||
break
|
||||
case VARIABLE_OPERATIONS.REMOVE:
|
||||
variablesStore.deleteVariable(payload.variableId)
|
||||
useVariablesStore.getState().deleteVariable(payload.variableId)
|
||||
break
|
||||
}
|
||||
} else if (target === OPERATION_TARGETS.WORKFLOW) {
|
||||
@@ -329,7 +342,7 @@ export function useCollaborativeWorkflow() {
|
||||
hasActiveDiff,
|
||||
isShowingDiff,
|
||||
})
|
||||
workflowStore.replaceWorkflowState(payload.state)
|
||||
useWorkflowStore.getState().replaceWorkflowState(payload.state)
|
||||
|
||||
// Extract and apply subblock values
|
||||
const subBlockValues: Record<string, Record<string, any>> = {}
|
||||
@@ -344,7 +357,7 @@ export function useCollaborativeWorkflow() {
|
||||
}
|
||||
)
|
||||
if (activeWorkflowId) {
|
||||
subBlockStore.setWorkflowValues(activeWorkflowId, subBlockValues)
|
||||
useSubBlockStore.getState().setWorkflowValues(activeWorkflowId, subBlockValues)
|
||||
}
|
||||
|
||||
logger.info('Successfully applied remote workflow state replacement')
|
||||
@@ -364,7 +377,9 @@ export function useCollaborativeWorkflow() {
|
||||
})
|
||||
|
||||
if (blocks && blocks.length > 0) {
|
||||
workflowStore.batchAddBlocks(blocks, edges || [], addedSubBlockValues || {})
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.batchAddBlocks(blocks, edges || [], addedSubBlockValues || {})
|
||||
}
|
||||
|
||||
logger.info('Successfully applied batch-add-blocks from remote user')
|
||||
@@ -378,7 +393,7 @@ export function useCollaborativeWorkflow() {
|
||||
})
|
||||
|
||||
if (ids && ids.length > 0) {
|
||||
workflowStore.batchRemoveBlocks(ids)
|
||||
useWorkflowStore.getState().batchRemoveBlocks(ids)
|
||||
}
|
||||
|
||||
logger.info('Successfully applied batch-remove-blocks from remote user')
|
||||
@@ -392,7 +407,7 @@ export function useCollaborativeWorkflow() {
|
||||
})
|
||||
|
||||
if (blockIds && blockIds.length > 0) {
|
||||
workflowStore.batchToggleEnabled(blockIds)
|
||||
useWorkflowStore.getState().batchToggleEnabled(blockIds)
|
||||
}
|
||||
|
||||
logger.info('Successfully applied batch-toggle-enabled from remote user')
|
||||
@@ -406,7 +421,7 @@ export function useCollaborativeWorkflow() {
|
||||
})
|
||||
|
||||
if (blockIds && blockIds.length > 0) {
|
||||
workflowStore.batchToggleHandles(blockIds)
|
||||
useWorkflowStore.getState().batchToggleHandles(blockIds)
|
||||
}
|
||||
|
||||
logger.info('Successfully applied batch-toggle-handles from remote user')
|
||||
@@ -420,7 +435,7 @@ export function useCollaborativeWorkflow() {
|
||||
})
|
||||
|
||||
if (updates && updates.length > 0) {
|
||||
workflowStore.batchUpdateBlocksWithParent(
|
||||
useWorkflowStore.getState().batchUpdateBlocksWithParent(
|
||||
updates.map(
|
||||
(u: { id: string; parentId: string; position: { x: number; y: number } }) => ({
|
||||
id: u.id,
|
||||
@@ -454,7 +469,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
try {
|
||||
// The setValue function automatically uses the active workflow ID
|
||||
subBlockStore.setValue(blockId, subblockId, value)
|
||||
useSubBlockStore.getState().setValue(blockId, subblockId, value)
|
||||
} catch (error) {
|
||||
logger.error('Error applying remote subblock update:', error)
|
||||
} finally {
|
||||
@@ -473,11 +488,11 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
try {
|
||||
if (field === 'name') {
|
||||
variablesStore.updateVariable(variableId, { name: value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { name: value })
|
||||
} else if (field === 'value') {
|
||||
variablesStore.updateVariable(variableId, { value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { value })
|
||||
} else if (field === 'type') {
|
||||
variablesStore.updateVariable(variableId, { type: value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { type: value })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error applying remote variable update:', error)
|
||||
@@ -626,14 +641,10 @@ export function useCollaborativeWorkflow() {
|
||||
onWorkflowReverted,
|
||||
onOperationConfirmed,
|
||||
onOperationFailed,
|
||||
workflowStore,
|
||||
subBlockStore,
|
||||
variablesStore,
|
||||
activeWorkflowId,
|
||||
confirmOperation,
|
||||
failOperation,
|
||||
emitWorkflowOperation,
|
||||
queue,
|
||||
])
|
||||
|
||||
const executeQueuedOperation = useCallback(
|
||||
@@ -710,14 +721,14 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchUpdatePositions(updates)
|
||||
useWorkflowStore.getState().batchUpdatePositions(updates)
|
||||
|
||||
if (options?.previousPositions && options.previousPositions.size > 0) {
|
||||
const moves = updates
|
||||
.filter((u) => options.previousPositions!.has(u.id))
|
||||
.map((u) => {
|
||||
const prev = options.previousPositions!.get(u.id)!
|
||||
const block = workflowStore.blocks[u.id]
|
||||
const block = useWorkflowStore.getState().blocks[u.id]
|
||||
return {
|
||||
blockId: u.id,
|
||||
before: prev,
|
||||
@@ -735,7 +746,7 @@ export function useCollaborativeWorkflow() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, workflowStore, undoRedo]
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeUpdateBlockName = useCallback(
|
||||
@@ -753,7 +764,7 @@ export function useCollaborativeWorkflow() {
|
||||
return { success: false, error: 'Block name cannot be empty' }
|
||||
}
|
||||
|
||||
const currentBlocks = workflowStore.blocks
|
||||
const currentBlocks = useWorkflowStore.getState().blocks
|
||||
const conflictingBlock = Object.entries(currentBlocks).find(
|
||||
([blockId, block]) => blockId !== id && normalizeName(block.name) === normalizedNewName
|
||||
)
|
||||
@@ -774,7 +785,7 @@ export function useCollaborativeWorkflow() {
|
||||
OPERATION_TARGETS.BLOCK,
|
||||
{ id, name: trimmedName },
|
||||
() => {
|
||||
const result = workflowStore.updateBlockName(id, trimmedName)
|
||||
const result = useWorkflowStore.getState().updateBlockName(id, trimmedName)
|
||||
|
||||
if (result.success && result.changedSubblocks.length > 0) {
|
||||
logger.info('Emitting cascaded subblock updates from block rename', {
|
||||
@@ -812,7 +823,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
return { success: true }
|
||||
},
|
||||
[executeQueuedOperation, workflowStore, addToQueue, activeWorkflowId, session?.user?.id]
|
||||
[executeQueuedOperation, addToQueue, activeWorkflowId, session?.user?.id]
|
||||
)
|
||||
|
||||
const collaborativeBatchToggleBlockEnabled = useCallback(
|
||||
@@ -823,7 +834,7 @@ export function useCollaborativeWorkflow() {
|
||||
const validIds: string[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const block = workflowStore.blocks[id]
|
||||
const block = useWorkflowStore.getState().blocks[id]
|
||||
if (block) {
|
||||
previousStates[id] = block.enabled
|
||||
validIds.push(id)
|
||||
@@ -845,11 +856,11 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchToggleEnabled(validIds)
|
||||
useWorkflowStore.getState().batchToggleEnabled(validIds)
|
||||
|
||||
undoRedo.recordBatchToggleEnabled(validIds, previousStates)
|
||||
},
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, workflowStore, undoRedo]
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeBatchUpdateParent = useCallback(
|
||||
@@ -869,7 +880,7 @@ export function useCollaborativeWorkflow() {
|
||||
if (updates.length === 0) return
|
||||
|
||||
const batchUpdates = updates.map((u) => {
|
||||
const block = workflowStore.blocks[u.blockId]
|
||||
const block = useWorkflowStore.getState().blocks[u.blockId]
|
||||
const oldParentId = block?.data?.parentId
|
||||
const oldPosition = block?.position || { x: 0, y: 0 }
|
||||
|
||||
@@ -886,11 +897,11 @@ export function useCollaborativeWorkflow() {
|
||||
// Collect all edge IDs to remove
|
||||
const edgeIdsToRemove = updates.flatMap((u) => u.affectedEdges.map((e) => e.id))
|
||||
if (edgeIdsToRemove.length > 0) {
|
||||
workflowStore.batchRemoveEdges(edgeIdsToRemove)
|
||||
useWorkflowStore.getState().batchRemoveEdges(edgeIdsToRemove)
|
||||
}
|
||||
|
||||
// Batch update positions and parents
|
||||
workflowStore.batchUpdateBlocksWithParent(
|
||||
useWorkflowStore.getState().batchUpdateBlocksWithParent(
|
||||
updates.map((u) => ({
|
||||
id: u.blockId,
|
||||
position: u.newPosition,
|
||||
@@ -920,22 +931,22 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
logger.debug('Batch updated parent for blocks', { updateCount: updates.length })
|
||||
},
|
||||
[isInActiveRoom, workflowStore, undoRedo, addToQueue, activeWorkflowId, session?.user?.id]
|
||||
[isInActiveRoom, undoRedo, addToQueue, activeWorkflowId, session?.user?.id]
|
||||
)
|
||||
|
||||
const collaborativeToggleBlockAdvancedMode = useCallback(
|
||||
(id: string) => {
|
||||
const block = workflowStore.blocks[id]
|
||||
const block = useWorkflowStore.getState().blocks[id]
|
||||
if (!block) return
|
||||
const newAdvancedMode = !block.advancedMode
|
||||
executeQueuedOperation(
|
||||
BLOCK_OPERATIONS.UPDATE_ADVANCED_MODE,
|
||||
OPERATION_TARGETS.BLOCK,
|
||||
{ id, advancedMode: newAdvancedMode },
|
||||
() => workflowStore.setBlockAdvancedMode(id, newAdvancedMode)
|
||||
() => useWorkflowStore.getState().setBlockAdvancedMode(id, newAdvancedMode)
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeSetBlockCanonicalMode = useCallback(
|
||||
@@ -944,10 +955,10 @@ export function useCollaborativeWorkflow() {
|
||||
BLOCK_OPERATIONS.UPDATE_CANONICAL_MODE,
|
||||
OPERATION_TARGETS.BLOCK,
|
||||
{ id, canonicalId, canonicalMode },
|
||||
() => workflowStore.setBlockCanonicalMode(id, canonicalId, canonicalMode)
|
||||
() => useWorkflowStore.getState().setBlockCanonicalMode(id, canonicalId, canonicalMode)
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeBatchToggleBlockHandles = useCallback(
|
||||
@@ -958,7 +969,7 @@ export function useCollaborativeWorkflow() {
|
||||
const validIds: string[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const block = workflowStore.blocks[id]
|
||||
const block = useWorkflowStore.getState().blocks[id]
|
||||
if (block) {
|
||||
previousStates[id] = block.horizontalHandles ?? false
|
||||
validIds.push(id)
|
||||
@@ -980,11 +991,11 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchToggleHandles(validIds)
|
||||
useWorkflowStore.getState().batchToggleHandles(validIds)
|
||||
|
||||
undoRedo.recordBatchToggleHandles(validIds, previousStates)
|
||||
},
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, workflowStore, undoRedo]
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeBatchAddEdges = useCallback(
|
||||
@@ -996,7 +1007,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
if (edges.length === 0) return false
|
||||
|
||||
const newEdges = filterNewEdges(edges, workflowStore.edges)
|
||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
||||
if (newEdges.length === 0) return false
|
||||
|
||||
const operationId = crypto.randomUUID()
|
||||
@@ -1012,7 +1023,7 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchAddEdges(newEdges)
|
||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
||||
|
||||
if (!options?.skipUndoRedo) {
|
||||
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||
@@ -1020,7 +1031,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
return true
|
||||
},
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, workflowStore, undoRedo]
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, isInActiveRoom, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeBatchRemoveEdges = useCallback(
|
||||
@@ -1036,10 +1047,10 @@ export function useCollaborativeWorkflow() {
|
||||
const validEdgeIds: string[] = []
|
||||
|
||||
for (const edgeId of edgeIds) {
|
||||
const edge = workflowStore.edges.find((e) => e.id === edgeId)
|
||||
const edge = useWorkflowStore.getState().edges.find((e) => e.id === edgeId)
|
||||
if (edge) {
|
||||
const sourceExists = workflowStore.blocks[edge.source]
|
||||
const targetExists = workflowStore.blocks[edge.target]
|
||||
const sourceExists = useWorkflowStore.getState().blocks[edge.source]
|
||||
const targetExists = useWorkflowStore.getState().blocks[edge.target]
|
||||
if (sourceExists && targetExists) {
|
||||
edgeSnapshots.push(edge)
|
||||
validEdgeIds.push(edgeId)
|
||||
@@ -1065,7 +1076,7 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchRemoveEdges(validEdgeIds)
|
||||
useWorkflowStore.getState().batchRemoveEdges(validEdgeIds)
|
||||
|
||||
if (!options?.skipUndoRedo && edgeSnapshots.length > 0) {
|
||||
undoRedo.recordBatchRemoveEdges(edgeSnapshots)
|
||||
@@ -1074,7 +1085,7 @@ export function useCollaborativeWorkflow() {
|
||||
logger.info('Batch removed edges', { count: validEdgeIds.length })
|
||||
return true
|
||||
},
|
||||
[isInActiveRoom, workflowStore, addToQueue, activeWorkflowId, session, undoRedo]
|
||||
[isInActiveRoom, addToQueue, activeWorkflowId, session, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeSetSubblockValue = useCallback(
|
||||
@@ -1086,8 +1097,32 @@ export function useCollaborativeWorkflow() {
|
||||
return
|
||||
}
|
||||
|
||||
// ALWAYS update local store first for immediate UI feedback
|
||||
useSubBlockStore.getState().setValue(blockId, subblockId, value)
|
||||
|
||||
// Handle dependent subblock clearing (recursive calls)
|
||||
try {
|
||||
const visited = options?._visited || new Set<string>()
|
||||
if (visited.has(subblockId)) return
|
||||
visited.add(subblockId)
|
||||
const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type
|
||||
const blockConfig = blockType ? getBlock(blockType) : null
|
||||
if (blockConfig?.subBlocks && Array.isArray(blockConfig.subBlocks)) {
|
||||
const dependents = blockConfig.subBlocks.filter(
|
||||
(sb: any) => Array.isArray(sb.dependsOn) && sb.dependsOn.includes(subblockId)
|
||||
)
|
||||
for (const dep of dependents) {
|
||||
if (!dep?.id || dep.id === subblockId) continue
|
||||
collaborativeSetSubblockValue(blockId, dep.id, '', { _visited: visited })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort; do not block on clearing
|
||||
}
|
||||
|
||||
// Only emit to socket if in active room
|
||||
if (!isInActiveRoom()) {
|
||||
logger.debug('Skipping subblock update - not in active workflow', {
|
||||
logger.debug('Local update applied, skipping socket emit - not in active workflow', {
|
||||
currentWorkflowId,
|
||||
activeWorkflowId,
|
||||
blockId,
|
||||
@@ -1110,30 +1145,8 @@ export function useCollaborativeWorkflow() {
|
||||
workflowId: currentActiveWorkflowId || '',
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
subBlockStore.setValue(blockId, subblockId, value)
|
||||
|
||||
try {
|
||||
const visited = options?._visited || new Set<string>()
|
||||
if (visited.has(subblockId)) return
|
||||
visited.add(subblockId)
|
||||
const blockType = useWorkflowStore.getState().blocks?.[blockId]?.type
|
||||
const blockConfig = blockType ? getBlock(blockType) : null
|
||||
if (blockConfig?.subBlocks && Array.isArray(blockConfig.subBlocks)) {
|
||||
const dependents = blockConfig.subBlocks.filter(
|
||||
(sb: any) => Array.isArray(sb.dependsOn) && sb.dependsOn.includes(subblockId)
|
||||
)
|
||||
for (const dep of dependents) {
|
||||
if (!dep?.id || dep.id === subblockId) continue
|
||||
collaborativeSetSubblockValue(blockId, dep.id, '', { _visited: visited })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Best-effort; do not block on clearing
|
||||
}
|
||||
},
|
||||
[
|
||||
subBlockStore,
|
||||
currentWorkflowId,
|
||||
activeWorkflowId,
|
||||
addToQueue,
|
||||
@@ -1159,7 +1172,7 @@ export function useCollaborativeWorkflow() {
|
||||
}
|
||||
|
||||
// Apply locally first (immediate UI feedback)
|
||||
subBlockStore.setValue(blockId, subblockId, value)
|
||||
useSubBlockStore.getState().setValue(blockId, subblockId, value)
|
||||
|
||||
// Use the operation queue but with immediate processing (no debouncing)
|
||||
const operationId = crypto.randomUUID()
|
||||
@@ -1175,29 +1188,22 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
},
|
||||
[
|
||||
subBlockStore,
|
||||
addToQueue,
|
||||
currentWorkflowId,
|
||||
activeWorkflowId,
|
||||
session?.user?.id,
|
||||
isInActiveRoom,
|
||||
]
|
||||
[addToQueue, currentWorkflowId, activeWorkflowId, session?.user?.id, isInActiveRoom]
|
||||
)
|
||||
|
||||
const collaborativeUpdateLoopType = useCallback(
|
||||
(loopId: string, loopType: 'for' | 'forEach' | 'while' | 'doWhile') => {
|
||||
const currentBlock = workflowStore.blocks[loopId]
|
||||
const currentBlock = useWorkflowStore.getState().blocks[loopId]
|
||||
if (!currentBlock || currentBlock.type !== 'loop') return
|
||||
|
||||
const childNodes = Object.values(workflowStore.blocks)
|
||||
const childNodes = Object.values(useWorkflowStore.getState().blocks)
|
||||
.filter((b) => b.data?.parentId === loopId)
|
||||
.map((b) => b.id)
|
||||
|
||||
const currentIterations = currentBlock.data?.count || 5
|
||||
const currentCollection = currentBlock.data?.collection || ''
|
||||
|
||||
const existingLoop = workflowStore.loops[loopId]
|
||||
const existingLoop = useWorkflowStore.getState().loops[loopId]
|
||||
const existingForEachItems = existingLoop?.forEachItems ?? currentCollection ?? ''
|
||||
const existingWhileCondition =
|
||||
existingLoop?.whileCondition ?? currentBlock.data?.whileCondition ?? ''
|
||||
@@ -1219,22 +1225,24 @@ export function useCollaborativeWorkflow() {
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: loopId, type: 'loop', config },
|
||||
() => {
|
||||
workflowStore.updateLoopType(loopId, loopType)
|
||||
workflowStore.setLoopForEachItems(loopId, existingForEachItems ?? '')
|
||||
workflowStore.setLoopWhileCondition(loopId, existingWhileCondition ?? '')
|
||||
workflowStore.setLoopDoWhileCondition(loopId, existingDoWhileCondition ?? '')
|
||||
useWorkflowStore.getState().updateLoopType(loopId, loopType)
|
||||
useWorkflowStore.getState().setLoopForEachItems(loopId, existingForEachItems ?? '')
|
||||
useWorkflowStore.getState().setLoopWhileCondition(loopId, existingWhileCondition ?? '')
|
||||
useWorkflowStore
|
||||
.getState()
|
||||
.setLoopDoWhileCondition(loopId, existingDoWhileCondition ?? '')
|
||||
}
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeUpdateParallelType = useCallback(
|
||||
(parallelId: string, parallelType: 'count' | 'collection') => {
|
||||
const currentBlock = workflowStore.blocks[parallelId]
|
||||
const currentBlock = useWorkflowStore.getState().blocks[parallelId]
|
||||
if (!currentBlock || currentBlock.type !== 'parallel') return
|
||||
|
||||
const childNodes = Object.values(workflowStore.blocks)
|
||||
const childNodes = Object.values(useWorkflowStore.getState().blocks)
|
||||
.filter((b) => b.data?.parentId === parallelId)
|
||||
.map((b) => b.id)
|
||||
|
||||
@@ -1261,22 +1269,22 @@ export function useCollaborativeWorkflow() {
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: parallelId, type: 'parallel', config },
|
||||
() => {
|
||||
workflowStore.updateParallelType(parallelId, parallelType)
|
||||
workflowStore.updateParallelCount(parallelId, newCount)
|
||||
workflowStore.updateParallelCollection(parallelId, newDistribution)
|
||||
useWorkflowStore.getState().updateParallelType(parallelId, parallelType)
|
||||
useWorkflowStore.getState().updateParallelCount(parallelId, newCount)
|
||||
useWorkflowStore.getState().updateParallelCollection(parallelId, newDistribution)
|
||||
}
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
// Unified iteration management functions - count and collection only
|
||||
const collaborativeUpdateIterationCount = useCallback(
|
||||
(nodeId: string, iterationType: 'loop' | 'parallel', count: number) => {
|
||||
const currentBlock = workflowStore.blocks[nodeId]
|
||||
const currentBlock = useWorkflowStore.getState().blocks[nodeId]
|
||||
if (!currentBlock || currentBlock.type !== iterationType) return
|
||||
|
||||
const childNodes = Object.values(workflowStore.blocks)
|
||||
const childNodes = Object.values(useWorkflowStore.getState().blocks)
|
||||
.filter((b) => b.data?.parentId === nodeId)
|
||||
.map((b) => b.id)
|
||||
|
||||
@@ -1296,7 +1304,7 @@ export function useCollaborativeWorkflow() {
|
||||
SUBFLOW_OPERATIONS.UPDATE,
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: nodeId, type: 'loop', config },
|
||||
() => workflowStore.updateLoopCount(nodeId, count)
|
||||
() => useWorkflowStore.getState().updateLoopCount(nodeId, count)
|
||||
)
|
||||
} else {
|
||||
const currentDistribution = currentBlock.data?.collection || ''
|
||||
@@ -1314,19 +1322,19 @@ export function useCollaborativeWorkflow() {
|
||||
SUBFLOW_OPERATIONS.UPDATE,
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: nodeId, type: 'parallel', config },
|
||||
() => workflowStore.updateParallelCount(nodeId, count)
|
||||
() => useWorkflowStore.getState().updateParallelCount(nodeId, count)
|
||||
)
|
||||
}
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeUpdateIterationCollection = useCallback(
|
||||
(nodeId: string, iterationType: 'loop' | 'parallel', collection: string) => {
|
||||
const currentBlock = workflowStore.blocks[nodeId]
|
||||
const currentBlock = useWorkflowStore.getState().blocks[nodeId]
|
||||
if (!currentBlock || currentBlock.type !== iterationType) return
|
||||
|
||||
const childNodes = Object.values(workflowStore.blocks)
|
||||
const childNodes = Object.values(useWorkflowStore.getState().blocks)
|
||||
.filter((b) => b.data?.parentId === nodeId)
|
||||
.map((b) => b.id)
|
||||
|
||||
@@ -1334,7 +1342,7 @@ export function useCollaborativeWorkflow() {
|
||||
const currentIterations = currentBlock.data?.count || 5
|
||||
const currentLoopType = currentBlock.data?.loopType || 'for'
|
||||
|
||||
const existingLoop = workflowStore.loops[nodeId]
|
||||
const existingLoop = useWorkflowStore.getState().loops[nodeId]
|
||||
let nextForEachItems = existingLoop?.forEachItems ?? currentBlock.data?.collection ?? ''
|
||||
let nextWhileCondition =
|
||||
existingLoop?.whileCondition ?? currentBlock.data?.whileCondition ?? ''
|
||||
@@ -1364,9 +1372,9 @@ export function useCollaborativeWorkflow() {
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: nodeId, type: 'loop', config },
|
||||
() => {
|
||||
workflowStore.setLoopForEachItems(nodeId, nextForEachItems ?? '')
|
||||
workflowStore.setLoopWhileCondition(nodeId, nextWhileCondition ?? '')
|
||||
workflowStore.setLoopDoWhileCondition(nodeId, nextDoWhileCondition ?? '')
|
||||
useWorkflowStore.getState().setLoopForEachItems(nodeId, nextForEachItems ?? '')
|
||||
useWorkflowStore.getState().setLoopWhileCondition(nodeId, nextWhileCondition ?? '')
|
||||
useWorkflowStore.getState().setLoopDoWhileCondition(nodeId, nextDoWhileCondition ?? '')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -1385,11 +1393,11 @@ export function useCollaborativeWorkflow() {
|
||||
SUBFLOW_OPERATIONS.UPDATE,
|
||||
OPERATION_TARGETS.SUBFLOW,
|
||||
{ id: nodeId, type: 'parallel', config },
|
||||
() => workflowStore.updateParallelCollection(nodeId, collection)
|
||||
() => useWorkflowStore.getState().updateParallelCollection(nodeId, collection)
|
||||
)
|
||||
}
|
||||
},
|
||||
[executeQueuedOperation, workflowStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeUpdateVariable = useCallback(
|
||||
@@ -1400,16 +1408,16 @@ export function useCollaborativeWorkflow() {
|
||||
{ variableId, field, value },
|
||||
() => {
|
||||
if (field === 'name') {
|
||||
variablesStore.updateVariable(variableId, { name: value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { name: value })
|
||||
} else if (field === 'value') {
|
||||
variablesStore.updateVariable(variableId, { value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { value })
|
||||
} else if (field === 'type') {
|
||||
variablesStore.updateVariable(variableId, { type: value })
|
||||
useVariablesStore.getState().updateVariable(variableId, { type: value })
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, variablesStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeAddVariable = useCallback(
|
||||
@@ -1417,7 +1425,7 @@ export function useCollaborativeWorkflow() {
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// Optimistically add to local store first
|
||||
variablesStore.addVariable(variableData, id)
|
||||
useVariablesStore.getState().addVariable(variableData, id)
|
||||
const processedVariable = useVariablesStore.getState().variables[id]
|
||||
|
||||
if (processedVariable) {
|
||||
@@ -1439,7 +1447,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
return id
|
||||
},
|
||||
[executeQueuedOperation, variablesStore]
|
||||
[executeQueuedOperation]
|
||||
)
|
||||
|
||||
const collaborativeDeleteVariable = useCallback(
|
||||
@@ -1451,11 +1459,11 @@ export function useCollaborativeWorkflow() {
|
||||
OPERATION_TARGETS.VARIABLE,
|
||||
{ variableId },
|
||||
() => {
|
||||
variablesStore.deleteVariable(variableId)
|
||||
useVariablesStore.getState().deleteVariable(variableId)
|
||||
}
|
||||
)
|
||||
},
|
||||
[executeQueuedOperation, variablesStore, cancelOperationsForVariable]
|
||||
[executeQueuedOperation, cancelOperationsForVariable]
|
||||
)
|
||||
|
||||
const collaborativeBatchAddBlocks = useCallback(
|
||||
@@ -1497,7 +1505,7 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchAddBlocks(blocks, edges, subBlockValues)
|
||||
useWorkflowStore.getState().batchAddBlocks(blocks, edges, subBlockValues)
|
||||
|
||||
if (!options?.skipUndoRedo) {
|
||||
undoRedo.recordBatchAddBlocks(blocks, edges, subBlockValues)
|
||||
@@ -1505,16 +1513,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
return true
|
||||
},
|
||||
[
|
||||
addToQueue,
|
||||
activeWorkflowId,
|
||||
session?.user?.id,
|
||||
isBaselineDiffView,
|
||||
isInActiveRoom,
|
||||
workflowStore,
|
||||
subBlockStore,
|
||||
undoRedo,
|
||||
]
|
||||
[addToQueue, activeWorkflowId, session?.user?.id, isBaselineDiffView, isInActiveRoom, undoRedo]
|
||||
)
|
||||
|
||||
const collaborativeBatchRemoveBlocks = useCallback(
|
||||
@@ -1530,7 +1529,7 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
const allBlocksToRemove = new Set<string>(blockIds)
|
||||
const findAllDescendants = (parentId: string) => {
|
||||
Object.entries(workflowStore.blocks).forEach(([blockId, block]) => {
|
||||
Object.entries(useWorkflowStore.getState().blocks).forEach(([blockId, block]) => {
|
||||
if (block.data?.parentId === parentId) {
|
||||
allBlocksToRemove.add(blockId)
|
||||
findAllDescendants(blockId)
|
||||
@@ -1544,7 +1543,10 @@ export function useCollaborativeWorkflow() {
|
||||
usePanelEditorStore.getState().clearCurrentBlock()
|
||||
}
|
||||
|
||||
const mergedBlocks = mergeSubblockState(workflowStore.blocks, activeWorkflowId || undefined)
|
||||
const mergedBlocks = mergeSubblockState(
|
||||
useWorkflowStore.getState().blocks,
|
||||
activeWorkflowId || undefined
|
||||
)
|
||||
const blockSnapshots: BlockState[] = []
|
||||
const subBlockValues: Record<string, Record<string, unknown>> = {}
|
||||
|
||||
@@ -1566,9 +1568,9 @@ export function useCollaborativeWorkflow() {
|
||||
}
|
||||
})
|
||||
|
||||
const edgeSnapshots = workflowStore.edges.filter(
|
||||
(e) => allBlocksToRemove.has(e.source) || allBlocksToRemove.has(e.target)
|
||||
)
|
||||
const edgeSnapshots = useWorkflowStore
|
||||
.getState()
|
||||
.edges.filter((e) => allBlocksToRemove.has(e.source) || allBlocksToRemove.has(e.target))
|
||||
|
||||
logger.info('Batch removing blocks collaboratively', {
|
||||
requestedCount: blockIds.length,
|
||||
@@ -1588,7 +1590,7 @@ export function useCollaborativeWorkflow() {
|
||||
userId: session?.user?.id || 'unknown',
|
||||
})
|
||||
|
||||
workflowStore.batchRemoveBlocks(blockIds)
|
||||
useWorkflowStore.getState().batchRemoveBlocks(blockIds)
|
||||
|
||||
if (!options?.skipUndoRedo && blockSnapshots.length > 0) {
|
||||
undoRedo.recordBatchRemoveBlocks(blockSnapshots, edgeSnapshots, subBlockValues)
|
||||
@@ -1601,7 +1603,6 @@ export function useCollaborativeWorkflow() {
|
||||
activeWorkflowId,
|
||||
session?.user?.id,
|
||||
isInActiveRoom,
|
||||
workflowStore,
|
||||
cancelOperationsForBlock,
|
||||
undoRedo,
|
||||
]
|
||||
@@ -1646,10 +1647,6 @@ export function useCollaborativeWorkflow() {
|
||||
collaborativeUpdateIterationCount,
|
||||
collaborativeUpdateIterationCollection,
|
||||
|
||||
// Direct access to stores for non-collaborative operations
|
||||
workflowStore,
|
||||
subBlockStore,
|
||||
|
||||
// Undo/Redo operations (wrapped to prevent recording moves during undo/redo)
|
||||
undo: useCallback(async () => {
|
||||
isUndoRedoInProgress.current = true
|
||||
|
||||
@@ -40,7 +40,6 @@ export function usePermissionConfig(): PermissionConfigResult {
|
||||
|
||||
const isBlockAllowed = useMemo(() => {
|
||||
return (blockType: string) => {
|
||||
// start_trigger should always be allowed (it should never be disabled)
|
||||
if (blockType === 'start_trigger') return true
|
||||
if (config.allowedIntegrations === null) return true
|
||||
return config.allowedIntegrations.includes(blockType)
|
||||
@@ -57,7 +56,6 @@ export function usePermissionConfig(): PermissionConfigResult {
|
||||
const filterBlocks = useMemo(() => {
|
||||
return <T extends { type: string }>(blocks: T[]): T[] => {
|
||||
if (config.allowedIntegrations === null) return blocks
|
||||
// start_trigger should always be included (it should never be disabled)
|
||||
return blocks.filter(
|
||||
(block) =>
|
||||
block.type === 'start_trigger' || config.allowedIntegrations!.includes(block.type)
|
||||
@@ -77,14 +75,26 @@ export function usePermissionConfig(): PermissionConfigResult {
|
||||
return featureFlagDisabled || config.disableInvitations
|
||||
}, [config.disableInvitations])
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
isInPermissionGroup,
|
||||
filterBlocks,
|
||||
filterProviders,
|
||||
isBlockAllowed,
|
||||
isProviderAllowed,
|
||||
isInvitationsDisabled,
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
config,
|
||||
isLoading,
|
||||
isInPermissionGroup,
|
||||
filterBlocks,
|
||||
filterProviders,
|
||||
isBlockAllowed,
|
||||
isProviderAllowed,
|
||||
isInvitationsDisabled,
|
||||
}),
|
||||
[
|
||||
config,
|
||||
isLoading,
|
||||
isInPermissionGroup,
|
||||
filterBlocks,
|
||||
filterProviders,
|
||||
isBlockAllowed,
|
||||
isProviderAllowed,
|
||||
isInvitationsDisabled,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user