diff --git a/apps/docs/content/docs/en/tools/github.mdx b/apps/docs/content/docs/en/tools/github.mdx index ba06eb6a4a..a97956d3bb 100644 --- a/apps/docs/content/docs/en/tools/github.mdx +++ b/apps/docs/content/docs/en/tools/github.mdx @@ -1557,11 +1557,74 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name, | `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 | +| ↳ `name` | string | Repository name | | ↳ `path` | string | File path | | ↳ `sha` | string | Blob SHA | -| ↳ `html_url` | string | GitHub web URL | -| ↳ `repository` | object | Repository object | +| ↳ `url` | string | API URL | +| ↳ `git_url` | string | Git blob URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `score` | number | Search relevance score | +| ↳ `repository` | object | Repository containing the code | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `name` | string | Repository name | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `html_url` | string | Profile page URL | +| ↳ `description` | string | Repository description | +| ↳ `fork` | boolean | Whether this is a fork | +| ↳ `url` | string | API URL | +| ↳ `owner` | object | Repository owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `description` | string | Repository description | +| ↳ `fork` | boolean | Whether this is a fork | +| ↳ `owner` | object | Repository owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `text_matches` | array | Text matches showing context | +| ↳ `object_url` | string | Object URL | +| ↳ `object_type` | string | Object type | +| ↳ `property` | string | Property matched | +| ↳ `fragment` | string | Text fragment with match | +| ↳ `matches` | array | Match indices | +| ↳ `text` | string | Matched text | +| ↳ `indices` | array | Start and end indices | +| ↳ `text` | string | Matched text | +| ↳ `indices` | array | Start and end indices | +| ↳ `object_url` | string | Object URL | +| ↳ `object_type` | string | Object type | +| ↳ `property` | string | Property matched | +| ↳ `fragment` | string | Text fragment with match | +| ↳ `matches` | array | Match indices | +| ↳ `text` | string | Matched text | +| ↳ `indices` | array | Start and end indices | +| ↳ `text` | string | Matched text | +| ↳ `indices` | array | Start and end indices | ### `github_search_commits` @@ -1585,12 +1648,100 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us | `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 | +| ↳ `sha` | string | Parent SHA | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `html_url` | string | Parent web URL | +| ↳ `url` | string | Parent API URL | +| ↳ `comments_url` | string | Comments API URL | +| ↳ `score` | number | Search relevance score | +| ↳ `commit` | object | Core commit data | +| ↳ `url` | string | Tree API URL | +| ↳ `message` | string | Commit message | +| ↳ `comment_count` | number | Number of comments | +| ↳ `author` | object | Git author | +| ↳ `name` | string | Author name | +| ↳ `email` | string | Author email | +| ↳ `date` | string | Author date \(ISO 8601\) | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `committer` | object | Git committer | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `tree` | object | Tree object | +| ↳ `sha` | string | Tree SHA | +| ↳ `url` | string | Tree API URL | +| ↳ `sha` | string | Tree SHA | +| ↳ `message` | string | Commit message | +| ↳ `comment_count` | number | Number of comments | +| ↳ `author` | object | GitHub user \(author\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `name` | string | Repository name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `committer` | object | GitHub user \(committer\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `tree` | object | Tree object | +| ↳ `sha` | string | Tree SHA | +| ↳ `url` | string | Tree API URL | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `repository` | object | Repository containing the commit | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `name` | string | Repository name | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `html_url` | string | Profile page URL | +| ↳ `description` | string | Repository description | +| ↳ `owner` | object | Repository owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `description` | string | Repository description | +| ↳ `owner` | object | Repository owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `parents` | array | Parent commits | +| ↳ `sha` | string | Parent SHA | +| ↳ `url` | string | Parent API URL | +| ↳ `html_url` | string | Parent web URL | ### `github_search_issues` @@ -1614,18 +1765,82 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne | `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 | +| ↳ `id` | number | Milestone ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `number` | number | Milestone number | +| ↳ `title` | string | Milestone title | +| ↳ `state` | string | State \(open or closed\) | +| ↳ `locked` | boolean | Whether issue is locked | | ↳ `html_url` | string | Web URL | +| ↳ `url` | string | API URL | +| ↳ `repository_url` | string | Repository API URL | +| ↳ `comments_url` | string | Comments API 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 | +| ↳ `comments` | number | Number of comments | +| ↳ `score` | number | Search relevance score | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `updated_at` | string | Last update timestamp | +| ↳ `closed_at` | string | Close timestamp | +| ↳ `user` | object | Issue author | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `labels` | array | Issue labels | +| ↳ `id` | number | Label ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `url` | string | API URL | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Hex color code | +| ↳ `default` | boolean | Whether this is a default label | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Milestone description | +| ↳ `color` | string | Hex color code | +| ↳ `default` | boolean | Whether this is a default label | +| ↳ `assignee` | object | Primary assignee | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `assignees` | array | All assignees | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `milestone` | object | Associated milestone | +| ↳ `id` | number | Milestone ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `number` | number | Milestone number | +| ↳ `title` | string | Milestone title | +| ↳ `description` | string | Milestone description | +| ↳ `state` | string | State \(open or closed\) | +| ↳ `html_url` | string | Web URL | +| ↳ `due_on` | string | Due date | +| ↳ `due_on` | string | Due date | +| ↳ `pull_request` | object | Pull request details \(if this is a PR\) | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Web URL | +| ↳ `diff_url` | string | Diff URL | +| ↳ `patch_url` | string | Patch URL | +| ↳ `diff_url` | string | Diff URL | +| ↳ `patch_url` | string | Patch URL | ### `github_search_repos` @@ -1649,16 +1864,46 @@ Search for repositories across GitHub. Use qualifiers like language:python, star | `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 | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `name` | string | License name | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `description` | string | Repository description | +| ↳ `html_url` | string | Profile page URL | +| ↳ `url` | string | API URL | +| ↳ `fork` | boolean | Whether this is a fork | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `updated_at` | string | Last update timestamp | +| ↳ `pushed_at` | string | Last push timestamp | +| ↳ `size` | number | Repository size in KB | +| ↳ `stargazers_count` | number | Number of stars | +| ↳ `watchers_count` | number | Number of watchers | +| ↳ `forks_count` | number | Number of forks | +| ↳ `open_issues_count` | number | Number of open issues | +| ↳ `language` | string | Primary programming language | +| ↳ `default_branch` | string | Default branch name | +| ↳ `score` | number | Search relevance score | +| ↳ `topics` | array | Repository topics | +| ↳ `license` | object | License information | +| ↳ `key` | string | License key \(e.g., mit\) | +| ↳ `name` | string | License name | +| ↳ `spdx_id` | string | SPDX identifier | +| ↳ `key` | string | License key \(e.g., mit\) | +| ↳ `spdx_id` | string | SPDX identifier | +| ↳ `owner` | object | Repository owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_search_users` @@ -1683,11 +1928,21 @@ Search for users and organizations on GitHub. Use qualifiers like type:user, typ | `incomplete_results` | boolean | Whether results are incomplete | | `items` | array | Array of user objects from GitHub API | | ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | | ↳ `login` | string | Username | -| ↳ `html_url` | string | Profile URL | -| ↳ `avatar_url` | string | Avatar URL | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `gravatar_id` | string | Gravatar ID | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `followers_url` | string | Followers API URL | +| ↳ `following_url` | string | Following API URL | +| ↳ `gists_url` | string | Gists API URL | +| ↳ `starred_url` | string | Starred API URL | +| ↳ `repos_url` | string | Repos API URL | +| ↳ `organizations_url` | string | Organizations API URL | | ↳ `type` | string | User or Organization | -| ↳ `site_admin` | boolean | Is site admin | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `score` | number | Search relevance score | ### `github_list_commits` @@ -1714,12 +1969,83 @@ List commits in a repository with optional filtering by SHA, path, author, commi | 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 | +| ↳ `sha` | string | Parent SHA | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `html_url` | string | Parent web URL | +| ↳ `url` | string | Parent API URL | +| ↳ `comments_url` | string | Comments API URL | +| ↳ `commit` | object | Core commit data | +| ↳ `url` | string | Tree API URL | +| ↳ `message` | string | Commit message | +| ↳ `comment_count` | number | Number of comments | +| ↳ `author` | object | Git author | +| ↳ `name` | string | Author name | +| ↳ `email` | string | Author email | +| ↳ `date` | string | Author date \(ISO 8601\) | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `committer` | object | Git committer | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `tree` | object | Tree object | +| ↳ `sha` | string | Tree SHA | +| ↳ `url` | string | Tree API URL | +| ↳ `sha` | string | Tree SHA | +| ↳ `verification` | object | Signature verification | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| ↳ `message` | string | Commit message | +| ↳ `comment_count` | number | Number of comments | +| ↳ `author` | object | GitHub user \(author\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `committer` | object | GitHub user \(committer\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `tree` | object | Tree object | +| ↳ `sha` | string | Tree SHA | +| ↳ `url` | string | Tree API URL | +| ↳ `verification` | object | Signature verification | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | | ↳ `parents` | array | Parent commits | +| ↳ `sha` | string | Parent SHA | +| ↳ `url` | string | Parent API URL | +| ↳ `html_url` | string | Parent web URL | | `count` | number | Number of commits returned | ### `github_get_commit` @@ -1740,13 +2066,70 @@ Get detailed information about a specific commit including files changed and sta | 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 | +| `node_id` | string | GraphQL node ID | +| `html_url` | string | GitHub web URL | +| `url` | string | API URL | +| `comments_url` | string | Comments API URL | +| `commit` | object | Core commit data | +| ↳ `url` | string | Tree API URL | +| ↳ `message` | string | Commit message | +| ↳ `comment_count` | number | Number of comments | +| ↳ `author` | object | Git author | +| ↳ `name` | string | Author name | +| ↳ `email` | string | Author email | +| ↳ `date` | string | Author date \(ISO 8601\) | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `committer` | object | Git committer | +| ↳ `name` | string | Committer name | +| ↳ `email` | string | Committer email | +| ↳ `date` | string | Commit date \(ISO 8601\) | +| ↳ `tree` | object | Tree object | +| ↳ `sha` | string | Tree SHA | +| ↳ `url` | string | Tree API URL | +| ↳ `sha` | string | Tree SHA | +| ↳ `verification` | object | Signature verification | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| ↳ `verified` | boolean | Whether signature is verified | +| ↳ `reason` | string | Verification reason | +| ↳ `signature` | string | GPG signature | +| ↳ `payload` | string | Signed payload | +| `author` | object | GitHub user \(author\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| `committer` | object | GitHub user \(committer\) | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | User or Organization | +| `stats` | object | Change statistics | +| ↳ `additions` | number | Lines added | +| ↳ `deletions` | number | Lines deleted | +| ↳ `total` | number | Total changes | +| `files` | array | Changed files \(diff entries\) | +| ↳ `sha` | string | Blob SHA | +| ↳ `filename` | string | File path | +| ↳ `status` | string | Change status \(added, removed, modified, renamed, copied, changed, unchanged\) | +| ↳ `additions` | number | Lines added | +| ↳ `deletions` | number | Lines deleted | +| ↳ `changes` | number | Total changes | +| ↳ `blob_url` | string | Blob URL | +| ↳ `raw_url` | string | Raw file URL | +| ↳ `contents_url` | string | Contents API URL | +| ↳ `patch` | string | Diff patch | +| ↳ `previous_filename` | string | Previous filename \(for renames\) | | `parents` | array | Parent commits | +| ↳ `sha` | string | Parent SHA | +| ↳ `url` | string | Parent API URL | +| ↳ `html_url` | string | Parent web URL | ### `github_compare_commits` @@ -1768,17 +2151,50 @@ Compare two commits or branches to see the diff, commits between them, and chang | 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 | +| `url` | string | API URL | +| `html_url` | string | GitHub web URL | +| `permalink_url` | string | Permanent link URL | +| `diff_url` | string | Diff download URL | +| `patch_url` | string | Patch download URL | +| `status` | string | Comparison status \(ahead, behind, identical, diverged\) | +| `ahead_by` | number | Commits head is ahead of base | +| `behind_by` | number | Commits head is behind base | +| `total_commits` | number | Total commits in comparison | +| `base_commit` | object | Base commit object | +| ↳ `sha` | string | Commit SHA | +| ↳ `html_url` | string | Web URL | +| ↳ `commit` | object | Commit data | +| ↳ `message` | string | Commit message | +| ↳ `author` | object | Git author \(name, email, date\) | +| ↳ `committer` | object | Git committer \(name, email, date\) | +| ↳ `message` | string | Commit message | +| ↳ `author` | object | GitHub user \(author\) | +| ↳ `committer` | object | GitHub user \(committer\) | +| `merge_base_commit` | object | Merge base commit object | +| ↳ `sha` | string | Commit SHA | +| ↳ `html_url` | string | Web URL | +| `commits` | array | Commits between base and head | +| ↳ `sha` | string | Commit SHA | +| ↳ `html_url` | string | Web URL | +| ↳ `commit` | object | Commit data | +| ↳ `message` | string | Commit message | +| ↳ `author` | object | Git author \(name, email, date\) | +| ↳ `committer` | object | Git committer \(name, email, date\) | +| ↳ `message` | string | Commit message | +| ↳ `author` | object | GitHub user | +| ↳ `committer` | object | GitHub user | +| `files` | array | Changed files \(diff entries\) | +| ↳ `sha` | string | Blob SHA | +| ↳ `filename` | string | File path | +| ↳ `status` | string | Change status \(added, removed, modified, renamed, copied, changed, unchanged\) | +| ↳ `additions` | number | Lines added | +| ↳ `deletions` | number | Lines deleted | +| ↳ `changes` | number | Total changes | +| ↳ `blob_url` | string | Blob URL | +| ↳ `raw_url` | string | Raw file URL | +| ↳ `contents_url` | string | Contents API URL | +| ↳ `patch` | string | Diff patch | +| ↳ `previous_filename` | string | Previous filename \(for renames\) | ### `github_create_gist` @@ -1799,15 +2215,30 @@ Create a new gist with one or more files | Parameter | Type | Description | | --------- | ---- | ----------- | | `id` | string | Gist ID | +| `node_id` | string | GraphQL node ID | +| `url` | string | API URL | | `html_url` | string | Web URL | +| `forks_url` | string | Forks API URL | +| `commits_url` | string | Commits API 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 | +| `description` | string | Gist description | +| `public` | boolean | Whether gist is public | +| `truncated` | boolean | Whether files are truncated | +| `comments` | number | Number of comments | +| `comments_url` | string | Comments API URL | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `files` | object | Files in the gist \(object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content\) | +| `owner` | object | Gist owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_get_gist` @@ -1825,14 +2256,37 @@ Get a gist by ID including its file contents | 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 | +| `node_id` | string | GraphQL node ID | +| `html_url` | string | GitHub web URL | +| `url` | string | API URL | +| `forks_url` | string | Forks API URL | +| `commits_url` | string | Commits API URL | +| `git_pull_url` | string | Git clone URL | +| `git_push_url` | string | Git push URL | +| `description` | string | Gist description | +| `public` | boolean | Whether gist is public | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `comments` | number | Number of comments | +| `comments_url` | string | Comments API URL | +| `truncated` | boolean | Whether content is truncated | +| `files` | object | Files in the gist \(keyed by filename\) | +| ↳ `filename` | string | File name | +| ↳ `type` | string | MIME type | +| ↳ `language` | string | Programming language | +| ↳ `raw_url` | string | Raw file URL | +| ↳ `size` | number | File size in bytes | +| ↳ `truncated` | boolean | Whether content is truncated | +| ↳ `content` | string | File content | +| `owner` | object | Gist owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_list_gists` @@ -1853,12 +2307,35 @@ List gists for a user or the authenticated user | 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 | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `forks_url` | string | Forks API URL | +| ↳ `commits_url` | string | Commits API URL | +| ↳ `git_pull_url` | string | Git pull URL | +| ↳ `git_push_url` | string | Git push URL | +| ↳ `description` | string | Gist description | +| ↳ `public` | boolean | Whether gist is public | +| ↳ `truncated` | boolean | Whether files are truncated | +| ↳ `comments` | number | Number of comments | +| ↳ `comments_url` | string | Comments API URL | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `updated_at` | string | Last update timestamp | +| ↳ `files` | object | Files in the gist \(object with filenames as keys, each containing filename, type, language, raw_url, size\) | +| ↳ `owner` | object | Gist owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | | `count` | number | Number of gists returned | ### `github_update_gist` @@ -1879,11 +2356,30 @@ Update a gist description or files. To delete a file, set its value to null in f | Parameter | Type | Description | | --------- | ---- | ----------- | | `id` | string | Gist ID | +| `node_id` | string | GraphQL node ID | +| `url` | string | API URL | | `html_url` | string | Web URL | -| `description` | string | Description | -| `public` | boolean | Is public | -| `updated_at` | string | Update date | -| `files` | object | Current files | +| `forks_url` | string | Forks API URL | +| `commits_url` | string | Commits API URL | +| `git_pull_url` | string | Git pull URL | +| `git_push_url` | string | Git push URL | +| `description` | string | Gist description | +| `public` | boolean | Whether gist is public | +| `truncated` | boolean | Whether files are truncated | +| `comments` | number | Number of comments | +| `comments_url` | string | Comments API URL | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `files` | object | Files in the gist \(object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content\) | +| `owner` | object | Gist owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_delete_gist` @@ -1982,14 +2478,43 @@ Fork a repository to your account or an organization | 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 | +| `node_id` | string | GraphQL node ID | +| `name` | string | Repository name | +| `full_name` | string | Full name \(owner/repo\) | +| `private` | boolean | Whether repository is private | +| `description` | string | Repository description | +| `html_url` | string | GitHub web URL | +| `url` | string | API URL | +| `clone_url` | string | HTTPS clone URL | +| `ssh_url` | string | SSH clone URL | +| `git_url` | string | Git protocol URL | +| `default_branch` | string | Default branch name | +| `fork` | boolean | Whether this is a fork | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `pushed_at` | string | Last push timestamp | +| `owner` | object | Fork owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| `parent` | object | Parent repository \(source of the fork\) | +| ↳ `id` | number | User ID | +| ↳ `full_name` | string | Full name | +| ↳ `html_url` | string | Web URL | +| ↳ `description` | string | Description | +| ↳ `owner` | object | Parent owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `login` | string | Username | +| `source` | object | Source repository \(ultimate origin\) | +| ↳ `id` | number | Repository ID | +| ↳ `full_name` | string | Full name | +| ↳ `html_url` | string | Web URL | ### `github_list_forks` @@ -2011,12 +2536,41 @@ List forks of a repository | 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 | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `name` | string | Repository name | +| ↳ `full_name` | string | Full name \(owner/repo\) | +| ↳ `private` | boolean | Whether repository is private | +| ↳ `description` | string | Repository description | +| ↳ `html_url` | string | Profile page URL | +| ↳ `url` | string | API URL | +| ↳ `fork` | boolean | Whether this is a fork | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `updated_at` | string | Last update timestamp | +| ↳ `pushed_at` | string | Last push timestamp | +| ↳ `size` | number | Repository size in KB | +| ↳ `stargazers_count` | number | Number of stars | +| ↳ `watchers_count` | number | Number of watchers | +| ↳ `forks_count` | number | Number of forks | +| ↳ `open_issues_count` | number | Number of open issues | +| ↳ `language` | string | Primary programming language | +| ↳ `default_branch` | string | Default branch name | +| ↳ `visibility` | string | Repository visibility | +| ↳ `archived` | boolean | Whether repository is archived | +| ↳ `disabled` | boolean | Whether repository is disabled | +| ↳ `owner` | object | Fork owner | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | | `count` | number | Number of forks returned | ### `github_create_milestone` @@ -2066,16 +2620,30 @@ Get a specific milestone by number | Parameter | Type | Description | | --------- | ---- | ----------- | +| `id` | number | Milestone ID | +| `node_id` | string | GraphQL node ID | | `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 | +| `title` | string | Milestone title | +| `description` | string | Milestone description | +| `state` | string | State \(open or closed\) | +| `url` | string | API URL | +| `html_url` | string | GitHub web URL | +| `labels_url` | string | Labels API URL | +| `due_on` | string | Due date \(ISO 8601\) | +| `open_issues` | number | Number of open issues | +| `closed_issues` | number | Number of closed issues | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `closed_at` | string | Close timestamp | +| `creator` | object | Milestone creator | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_list_milestones` @@ -2099,12 +2667,34 @@ List milestones in a repository | Parameter | Type | Description | | --------- | ---- | ----------- | | `items` | array | Array of milestone objects from GitHub API | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | | ↳ `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 | +| ↳ `title` | string | Milestone title | +| ↳ `description` | string | Milestone description | +| ↳ `state` | string | State \(open or closed\) | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `labels_url` | string | Labels API URL | +| ↳ `due_on` | string | Due date \(ISO 8601\) | +| ↳ `open_issues` | number | Number of open issues | +| ↳ `closed_issues` | number | Number of closed issues | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `updated_at` | string | Last update timestamp | +| ↳ `closed_at` | string | Close timestamp | +| ↳ `creator` | object | Milestone creator | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | +| ↳ `login` | string | Username | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | | `count` | number | Number of milestones returned | ### `github_update_milestone` @@ -2128,14 +2718,30 @@ Update a milestone in a repository | Parameter | Type | Description | | --------- | ---- | ----------- | +| `id` | number | Milestone ID | +| `node_id` | string | GraphQL node ID | | `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 | +| `title` | string | Milestone title | +| `description` | string | Milestone description | +| `state` | string | State \(open or closed\) | +| `url` | string | API URL | +| `html_url` | string | GitHub web URL | +| `labels_url` | string | Labels API URL | +| `due_on` | string | Due date \(ISO 8601\) | +| `open_issues` | number | Number of open issues | +| `closed_issues` | number | Number of closed issues | +| `created_at` | string | Creation timestamp | +| `updated_at` | string | Last update timestamp | +| `closed_at` | string | Close timestamp | +| `creator` | object | Milestone creator | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_delete_milestone` @@ -2176,9 +2782,18 @@ Add a reaction to an issue | Parameter | Type | Description | | --------- | ---- | ----------- | | `id` | number | Reaction ID | +| `node_id` | string | GraphQL node ID | +| `content` | string | Reaction type \(+1, -1, laugh, confused, heart, hooray, rocket, eyes\) | +| `created_at` | string | Creation timestamp | | `user` | object | User who reacted | -| `content` | string | Reaction type | -| `created_at` | string | Creation date | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_delete_issue_reaction` @@ -2220,9 +2835,18 @@ Add a reaction to an issue comment | Parameter | Type | Description | | --------- | ---- | ----------- | | `id` | number | Reaction ID | +| `node_id` | string | GraphQL node ID | +| `content` | string | Reaction type \(+1, -1, laugh, confused, heart, hooray, rocket, eyes\) | +| `created_at` | string | Creation timestamp | | `user` | object | User who reacted | -| `content` | string | Reaction type | -| `created_at` | string | Creation date | +| ↳ `login` | string | Username | +| ↳ `id` | number | User ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | ### `github_delete_comment_reaction` @@ -2326,9 +2950,18 @@ List users who have starred a repository | `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 | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `gravatar_id` | string | Gravatar ID | +| ↳ `url` | string | API URL | +| ↳ `html_url` | string | Profile page URL | +| ↳ `followers_url` | string | Followers API URL | +| ↳ `following_url` | string | Following API URL | +| ↳ `gists_url` | string | Gists API URL | +| ↳ `starred_url` | string | Starred API URL | +| ↳ `repos_url` | string | Repos API URL | | ↳ `type` | string | User or Organization | +| ↳ `site_admin` | boolean | GitHub staff indicator | | `count` | number | Number of stargazers returned | diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index c68a86f345..4a56b640e9 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -66,9 +66,9 @@ List files and folders in Google Drive with complete metadata | --------- | ---- | ----------- | | `files` | array | Array of file metadata objects from Google Drive | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `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 | @@ -135,6 +135,7 @@ Get metadata for a specific file in Google Drive by its ID | --------- | ---- | ----------- | | `file` | json | The file metadata | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `name` | string | File name | | ↳ `mimeType` | string | MIME type | | ↳ `description` | string | File description | @@ -175,9 +176,9 @@ Create a new folder in Google Drive with complete metadata returned | --------- | ---- | ----------- | | `file` | object | Complete created folder metadata from Google Drive | | ↳ `id` | string | Google Drive folder ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `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 | @@ -233,9 +234,9 @@ Upload a file to Google Drive with complete metadata returned | --------- | ---- | ----------- | | `file` | object | Complete uploaded file metadata from Google Drive | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `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 | @@ -309,9 +310,9 @@ Download a file from Google Drive with complete metadata (exports Google Workspa | ↳ `size` | number | File size in bytes | | `metadata` | object | Complete file metadata from Google Drive | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `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 | @@ -380,6 +381,7 @@ Create a copy of a file in Google Drive | --------- | ---- | ----------- | | `file` | json | The copied file metadata | | ↳ `id` | string | Google Drive file ID of the copy | +| ↳ `kind` | string | Resource type identifier | | ↳ `name` | string | File name | | ↳ `mimeType` | string | MIME type | | ↳ `webViewLink` | string | URL to view in browser | @@ -410,6 +412,7 @@ Update file metadata in Google Drive (rename, move, star, add description) | --------- | ---- | ----------- | | `file` | json | The updated file metadata | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `name` | string | File name | | ↳ `mimeType` | string | MIME type | | ↳ `description` | string | File description | @@ -434,34 +437,13 @@ Move a file to the trash in Google Drive (can be restored later) | --------- | ---- | ----------- | | `file` | json | The trashed file metadata | | ↳ `id` | string | Google Drive file ID | +| ↳ `kind` | string | Resource type identifier | | ↳ `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) @@ -557,6 +539,7 @@ List all permissions (who has access) for a file in Google Drive | ↳ `allowFileDiscovery` | boolean | Whether file is discoverable by grantee | | ↳ `pendingOwner` | boolean | Whether ownership transfer is pending | | ↳ `permissionDetails` | json | Details about inherited permissions | +| `nextPageToken` | string | Token for fetching the next page of permissions | ### `google_drive_get_about` diff --git a/apps/docs/content/docs/en/tools/google_forms.mdx b/apps/docs/content/docs/en/tools/google_forms.mdx index 3a43f590da..0cbdb357b6 100644 --- a/apps/docs/content/docs/en/tools/google_forms.mdx +++ b/apps/docs/content/docs/en/tools/google_forms.mdx @@ -51,13 +51,17 @@ Retrieve a single response or list responses from a Google Form | Parameter | Type | Description | | --------- | ---- | ----------- | -| `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 | +| `responses` | array | Array of form responses \(when no responseId provided\) | +| ↳ `responseId` | string | Unique response ID | +| ↳ `createTime` | string | When the response was created | +| ↳ `lastSubmittedTime` | string | When the response was last submitted | +| ↳ `answers` | json | Map of question IDs to answer values | +| `response` | object | Single form response \(when responseId is provided\) | +| ↳ `responseId` | string | Unique response ID | +| ↳ `createTime` | string | When the response was created | +| ↳ `lastSubmittedTime` | string | When the response was last submitted | +| ↳ `answers` | json | Map of question IDs to answer values | +| `raw` | json | Raw API response data | ### `google_forms_get_form` @@ -126,8 +130,48 @@ Apply multiple updates to a form (add items, update info, change settings, etc.) | 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\) | +| `writeControl` | object | Write control information with revision IDs | +| ↳ `requiredRevisionId` | string | Required revision ID for conflict detection | +| ↳ `targetRevisionId` | string | Target revision ID | +| `form` | object | The updated form \(if includeFormInResponse was true\) | +| ↳ `formId` | string | The form ID | +| ↳ `info` | object | Form info containing title and description | +| ↳ `title` | string | The form title visible to responders | +| ↳ `description` | string | The form description | +| ↳ `documentTitle` | string | The document title visible in Drive | +| ↳ `title` | string | Item title | +| ↳ `description` | string | Item description | +| ↳ `documentTitle` | string | The document title visible in Drive | +| ↳ `settings` | object | Form settings | +| ↳ `quizSettings` | object | Quiz settings | +| ↳ `isQuiz` | boolean | Whether the form is a quiz | +| ↳ `isQuiz` | boolean | Whether the form is a quiz | +| ↳ `emailCollectionType` | string | Email collection type | +| ↳ `quizSettings` | object | Quiz settings | +| ↳ `isQuiz` | boolean | Whether the form is a quiz | +| ↳ `isQuiz` | boolean | Whether the form is a quiz | +| ↳ `emailCollectionType` | string | Email collection type | +| ↳ `itemId` | string | Item ID | +| ↳ `questionItem` | json | Question item configuration | +| ↳ `questionGroupItem` | json | Question group configuration | +| ↳ `pageBreakItem` | json | Page break configuration | +| ↳ `textItem` | json | Text item configuration | +| ↳ `imageItem` | json | Image item configuration | +| ↳ `videoItem` | json | Video item configuration | +| ↳ `revisionId` | string | The revision ID of the form | +| ↳ `responderUri` | string | The URI to share with responders | +| ↳ `linkedSheetId` | string | The ID of the linked Google Sheet | +| ↳ `publishSettings` | object | Form publish settings | +| ↳ `publishState` | object | Current publish state | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses | +| ↳ `publishState` | object | Current publish state | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses | +| ↳ `isPublished` | boolean | Whether the form is published | +| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses | ### `google_forms_set_publish_settings` diff --git a/apps/docs/content/docs/en/tools/google_slides.mdx b/apps/docs/content/docs/en/tools/google_slides.mdx index 20df0ba2a5..3d826a257c 100644 --- a/apps/docs/content/docs/en/tools/google_slides.mdx +++ b/apps/docs/content/docs/en/tools/google_slides.mdx @@ -194,9 +194,14 @@ Get detailed information about a specific slide/page in a Google Slides presenta | --------- | ---- | ----------- | | `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 | +| `pageElements` | array | Array of page elements \(shapes, images, tables, etc.\) on this page | +| `slideProperties` | object | Properties specific to slides \(layout, master, notes\) | +| ↳ `layoutObjectId` | string | Object ID of the layout this slide is based on | +| ↳ `masterObjectId` | string | Object ID of the master this slide is based on | +| ↳ `notesPage` | json | The notes page associated with the slide | +| `metadata` | object | Operation metadata including presentation ID and URL | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | ### `google_slides_delete_object` @@ -215,7 +220,9 @@ Delete a page element (shape, image, table, etc.) or an entire slide from a Goog | --------- | ---- | ----------- | | `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 | +| `metadata` | object | Operation metadata including presentation ID and URL | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | ### `google_slides_duplicate_object` @@ -235,7 +242,10 @@ Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presen | Parameter | Type | Description | | --------- | ---- | ----------- | | `duplicatedObjectId` | string | The object ID of the newly created duplicate | -| `metadata` | json | Operation metadata including presentation ID and source object ID | +| `metadata` | object | Operation metadata including presentation ID and source object ID | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `sourceObjectId` | string | The original object ID that was duplicated | +| ↳ `url` | string | URL to the presentation | ### `google_slides_update_slides_position` @@ -256,7 +266,9 @@ Move one or more slides to a new position in a Google Slides presentation | `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 | +| `metadata` | object | Operation metadata including presentation ID and URL | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | ### `google_slides_create_table` @@ -282,7 +294,10 @@ Create a new table on a slide in a Google Slides presentation | `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 | +| `metadata` | object | Operation metadata including presentation ID and page object ID | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `pageObjectId` | string | The page object ID where the table was created | +| ↳ `url` | string | URL to the presentation | ### `google_slides_create_shape` @@ -306,7 +321,10 @@ Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Googl | --------- | ---- | ----------- | | `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 | +| `metadata` | object | Operation metadata including presentation ID and page object ID | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `pageObjectId` | string | The page object ID where the shape was created | +| ↳ `url` | string | URL to the presentation | ### `google_slides_insert_text` @@ -328,6 +346,8 @@ Insert text into a shape or table cell in a Google Slides presentation. Use this | `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 | +| `metadata` | object | Operation metadata including presentation ID and URL | +| ↳ `presentationId` | string | The presentation ID | +| ↳ `url` | string | URL to the presentation | diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 30a2bd79f3..3776f322f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -409,7 +409,11 @@ export function ChatDeploy({ Delete Chat

- Are you sure you want to delete this chat?{' '} + Are you sure you want to delete{' '} + + {existingChat?.title || 'this chat'} + + ?{' '} This will remove the chat at "{getEmailDomain()}/chat/{existingChat?.identifier}" and make it unavailable to all users. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx index cd8d8755a3..6ff75f6f61 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx @@ -375,8 +375,11 @@ export function TemplateDeploy({ Delete Template

- Are you sure you want to delete this template?{' '} - This action cannot be undone. + Are you sure you want to delete{' '} + + {existingTemplate?.name || formData.name || 'this template'} + + ? This action cannot be undone.

diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index 3db8509ec3..d03cc72106 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -846,7 +846,11 @@ export function DeployModal({ Delete A2A Agent

- Are you sure you want to delete this agent?{' '} + Are you sure you want to delete{' '} + + {existingA2aAgent?.name || 'this agent'} + + ?{' '} This will permanently remove the agent configuration. diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index dd0ad1dad7..b6e7aa4cbb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -41,6 +41,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/calendar': 'View and manage calendar', 'https://www.googleapis.com/auth/userinfo.email': 'View email address', 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', + 'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms', 'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms', 'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery', 'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx index db454dc432..ba34435672 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/components/keyboard-navigation-handler.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo } from 'react' import { usePopoverContext } from '@/components/emcn' -import type { BlockTagGroup, NestedBlockTagGroup } from '../types' +import { useNestedNavigation } from '../tag-dropdown' +import type { BlockTagGroup, NestedBlockTagGroup, NestedTag } from '../types' /** * Keyboard navigation handler component that uses popover context @@ -15,6 +16,90 @@ interface KeyboardNavigationHandlerProps { handleTagSelect: (tag: string, group?: BlockTagGroup) => void } +/** + * Recursively finds a folder in nested tags by its ID + */ +const findFolderInNested = ( + nestedTags: NestedTag[], + blockId: string, + targetFolderId: string +): NestedTag | null => { + for (const nestedTag of nestedTags) { + const folderId = `${blockId}-${nestedTag.key}` + if (folderId === targetFolderId) { + return nestedTag + } + if (nestedTag.nestedChildren) { + const found = findFolderInNested(nestedTag.nestedChildren, blockId, targetFolderId) + if (found) return found + } + } + return null +} + +/** + * Recursively finds folder info for a tag that can be expanded. + * Returns both the folder metadata and the NestedTag object for navigation. + */ +const findFolderInfoForTag = ( + nestedTags: NestedTag[], + targetTag: string, + group: NestedBlockTagGroup +): { + id: string + title: string + parentTag: string + group: NestedBlockTagGroup + nestedTag: NestedTag +} | null => { + for (const nestedTag of nestedTags) { + if ( + nestedTag.parentTag === targetTag && + (nestedTag.children?.length || nestedTag.nestedChildren?.length) + ) { + return { + id: `${group.blockId}-${nestedTag.key}`, + title: nestedTag.display, + parentTag: nestedTag.parentTag, + group, + nestedTag, + } + } + if (nestedTag.nestedChildren) { + const found = findFolderInfoForTag(nestedTag.nestedChildren, targetTag, group) + if (found) return found + } + } + return null +} + +/** + * Recursively checks if a tag is a child of any folder. + * This includes both leaf children and nested folder parent tags. + */ +const isChildOfAnyFolder = (nestedTags: NestedTag[], tag: string): boolean => { + for (const nestedTag of nestedTags) { + if (nestedTag.children) { + for (const child of nestedTag.children) { + if (child.fullTag === tag) { + return true + } + } + } + if (nestedTag.nestedChildren) { + for (const nestedChild of nestedTag.nestedChildren) { + if (nestedChild.parentTag === tag) { + return true + } + } + if (isChildOfAnyFolder(nestedTag.nestedChildren, tag)) { + return true + } + } + } + return false +} + export const KeyboardNavigationHandler: React.FC = ({ visible, selectedIndex, @@ -23,55 +108,66 @@ export const KeyboardNavigationHandler: React.FC nestedBlockTagGroups, handleTagSelect, }) => { - const { openFolder, closeFolder, isInFolder, currentFolder } = usePopoverContext() + const { openFolder, closeFolder, isInFolder, currentFolder, setKeyboardNav } = usePopoverContext() + const nestedNav = useNestedNavigation() const visibleIndices = useMemo(() => { const indices: number[] = [] + const nestedPath = nestedNav?.nestedPath ?? [] if (isInFolder && currentFolder) { - for (const group of nestedBlockTagGroups) { - for (const nestedTag of group.nestedTags) { - const folderId = `${group.blockId}-${nestedTag.key}` - if (folderId === currentFolder && nestedTag.children) { - // First, add the parent tag itself (so it's navigable as the first item) - if (nestedTag.parentTag) { - const parentIdx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) - if (parentIdx >= 0) { - indices.push(parentIdx) - } + let currentNestedTag: NestedTag | null = null + + if (nestedPath.length > 0) { + currentNestedTag = nestedPath[nestedPath.length - 1] + } else { + for (const group of nestedBlockTagGroups) { + const folder = findFolderInNested(group.nestedTags, group.blockId, currentFolder) + if (folder) { + currentNestedTag = folder + break + } + } + } + + if (currentNestedTag) { + if (currentNestedTag.parentTag) { + const parentIdx = flatTagList.findIndex( + (item) => item.tag === currentNestedTag!.parentTag + ) + if (parentIdx >= 0) { + indices.push(parentIdx) + } + } + if (currentNestedTag.children) { + for (const child of currentNestedTag.children) { + const idx = flatTagList.findIndex((item) => item.tag === child.fullTag) + if (idx >= 0) { + indices.push(idx) } - // Then add all children - for (const child of nestedTag.children) { - const idx = flatTagList.findIndex((item) => item.tag === child.fullTag) + } + } + if (currentNestedTag.nestedChildren) { + for (const nestedChild of currentNestedTag.nestedChildren) { + if (nestedChild.parentTag) { + const idx = flatTagList.findIndex((item) => item.tag === nestedChild.parentTag) if (idx >= 0) { indices.push(idx) } } - break } } } } else { - // We're at root level, show all non-child items - // (variables and parent tags, but not their children) for (let i = 0; i < flatTagList.length; i++) { const tag = flatTagList[i].tag - // Check if this is a child of a parent folder let isChild = false for (const group of nestedBlockTagGroups) { - for (const nestedTag of group.nestedTags) { - if (nestedTag.children) { - for (const child of nestedTag.children) { - if (child.fullTag === tag) { - isChild = true - break - } - } - } - if (isChild) break + if (isChildOfAnyFolder(group.nestedTags, tag)) { + isChild = true + break } - if (isChild) break } if (!isChild) { @@ -81,16 +177,16 @@ export const KeyboardNavigationHandler: React.FC } return indices - }, [isInFolder, currentFolder, flatTagList, nestedBlockTagGroups]) + }, [isInFolder, currentFolder, flatTagList, nestedBlockTagGroups, nestedNav]) + + const nestedPathLength = nestedNav?.nestedPath.length ?? 0 - // Auto-select first visible item when entering/exiting folders useEffect(() => { if (!visible || visibleIndices.length === 0) return - if (!visibleIndices.includes(selectedIndex)) { - setSelectedIndex(visibleIndices[0]) - } - }, [visible, isInFolder, currentFolder, visibleIndices, selectedIndex, setSelectedIndex]) + setSelectedIndex(visibleIndices[0]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible, isInFolder, currentFolder, nestedPathLength]) useEffect(() => { if (!visible || !flatTagList.length) return @@ -117,89 +213,98 @@ export const KeyboardNavigationHandler: React.FC id: string title: string parentTag: string - group: BlockTagGroup + group: NestedBlockTagGroup + nestedTag: NestedTag } | null = null if (selected) { for (const group of nestedBlockTagGroups) { - for (const nestedTag of group.nestedTags) { - if ( - nestedTag.parentTag === selected.tag && - nestedTag.children && - nestedTag.children.length > 0 - ) { - currentFolderInfo = { - id: `${selected.group?.blockId}-${nestedTag.key}`, - title: nestedTag.display, - parentTag: nestedTag.parentTag, - group, - } - break - } + const folderInfo = findFolderInfoForTag(group.nestedTags, selected.tag, group) + if (folderInfo) { + currentFolderInfo = folderInfo + break } - if (currentFolderInfo) break } } + const scrollIntoView = () => { + setTimeout(() => { + const selectedItem = document.querySelector( + '[data-radix-popper-content-wrapper] [aria-selected="true"]' + ) + if (selectedItem) { + selectedItem.scrollIntoView({ behavior: 'auto', block: 'nearest' }) + } + }, 0) + } + switch (e.key) { case 'ArrowDown': e.preventDefault() e.stopPropagation() + setKeyboardNav(true) if (visibleIndices.length > 0) { const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + let newIndex: number if (currentVisibleIndex === -1) { - setSelectedIndex(visibleIndices[0]) + newIndex = visibleIndices[0] } else if (currentVisibleIndex < visibleIndices.length - 1) { - setSelectedIndex(visibleIndices[currentVisibleIndex + 1]) + newIndex = visibleIndices[currentVisibleIndex + 1] + } else { + newIndex = visibleIndices[0] } + setSelectedIndex(newIndex) + scrollIntoView() } break case 'ArrowUp': e.preventDefault() e.stopPropagation() + setKeyboardNav(true) if (visibleIndices.length > 0) { const currentVisibleIndex = visibleIndices.indexOf(selectedIndex) + let newIndex: number if (currentVisibleIndex === -1) { - setSelectedIndex(visibleIndices[0]) + newIndex = visibleIndices[visibleIndices.length - 1] } else if (currentVisibleIndex > 0) { - setSelectedIndex(visibleIndices[currentVisibleIndex - 1]) + newIndex = visibleIndices[currentVisibleIndex - 1] + } else { + newIndex = visibleIndices[visibleIndices.length - 1] } + setSelectedIndex(newIndex) + scrollIntoView() } break case 'Enter': e.preventDefault() e.stopPropagation() if (selected && selectedIndex >= 0 && selectedIndex < flatTagList.length) { - if (currentFolderInfo && !isInFolder) { - // It's a folder, open it + handleTagSelect(selected.tag, selected.group) + } + break + case 'ArrowRight': + if (currentFolderInfo) { + e.preventDefault() + e.stopPropagation() + if (isInFolder && nestedNav) { + nestedNav.navigateIn(currentFolderInfo.nestedTag, currentFolderInfo.group) + } else { openFolderWithSelection( currentFolderInfo.id, currentFolderInfo.title, currentFolderInfo.parentTag, currentFolderInfo.group ) - } else { - // Not a folder, select it - handleTagSelect(selected.tag, selected.group) } } break - case 'ArrowRight': - if (currentFolderInfo && !isInFolder) { - e.preventDefault() - e.stopPropagation() - openFolderWithSelection( - currentFolderInfo.id, - currentFolderInfo.title, - currentFolderInfo.parentTag, - currentFolderInfo.group - ) - } - break case 'ArrowLeft': if (isInFolder) { e.preventDefault() e.stopPropagation() + if (nestedNav?.navigateBack()) { + return + } closeFolder() let firstRootIndex = 0 for (let i = 0; i < flatTagList.length; i++) { @@ -239,6 +344,8 @@ export const KeyboardNavigationHandler: React.FC isInFolder, setSelectedIndex, handleTagSelect, + nestedNav, + setKeyboardNav, ]) return null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index 4ff7205535..d5fde31199 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -1,12 +1,9 @@ -import type React from 'react' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { createLogger } from '@sim/logger' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { RepeatIcon, SplitIcon } from 'lucide-react' import { useShallow } from 'zustand/react/shallow' import { Popover, PopoverAnchor, - PopoverBackButton, PopoverContent, PopoverDivider, PopoverFolder, @@ -33,6 +30,7 @@ import type { BlockTagGroup, NestedBlockTagGroup, NestedTag, + NestedTagChild, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types' import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import { getBlock } from '@/blocks' @@ -45,7 +43,30 @@ import { normalizeName } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { BlockState } from '@/stores/workflows/workflow/types' -const logger = createLogger('TagDropdown') +/** + * Context for sharing nested navigation state between components. + * This enables unlimited nesting depth with a single back button. + */ +interface NestedNavigationContextValue { + /** Stack of nested tags representing current navigation depth */ + nestedPath: NestedTag[] + /** Navigate into a nested folder */ + navigateIn: (tag: NestedTag, group: NestedBlockTagGroup) => void + /** Navigate back one level, returns true if navigation happened, false if at root */ + navigateBack: () => boolean + /** Register the base folder when it opens */ + registerFolder: ( + folderId: string, + folderTitle: string, + baseTag: NestedTag, + group: NestedBlockTagGroup + ) => void +} + +const NestedNavigationContext = React.createContext(null) + +/** Hook to access nested navigation state from child components */ +export const useNestedNavigation = () => React.useContext(NestedNavigationContext) /** * Props for the TagDropdown component @@ -299,6 +320,101 @@ const getCaretViewportPosition = ( * @param color - Background color for the icon container * @returns A styled icon element */ +/** + * Tree node for building nested tag hierarchy + */ +interface TagTreeNode { + key: string + fullTag?: string + children: Map +} + +/** + * Builds a recursive tree structure from flat tag paths. + * Converts tags like `blockname.form.info.title` into a nested tree. + * + * @param tags - Array of dot-separated tag paths + * @param blockName - The normalized block name (first segment of tags) + * @returns Array of NestedTag with recursive nestedChildren + */ +const buildNestedTagTree = (tags: string[], blockName: string): NestedTag[] => { + const root: TagTreeNode = { key: 'root', children: new Map() } + + for (const tag of tags) { + const parts = tag.split('.') + if (parts.length < 2) continue + + const pathParts = parts.slice(1) + let current = root + + for (let i = 0; i < pathParts.length; i++) { + const part = pathParts[i] + if (!current.children.has(part)) { + current.children.set(part, { + key: part, + children: new Map(), + }) + } + const node = current.children.get(part)! + + if (i === pathParts.length - 1) { + node.fullTag = tag + } + current = node + } + } + + const convertToNestedTags = ( + node: TagTreeNode, + parentPath: string, + blockPrefix: string + ): NestedTag[] => { + const result: NestedTag[] = [] + + for (const [key, child] of node.children) { + const currentPath = parentPath ? `${parentPath}.${key}` : key + const parentTag = `${blockPrefix}.${currentPath}` + + if (child.children.size === 0) { + result.push({ + key: currentPath, + display: key, + fullTag: child.fullTag || parentTag, + }) + } else { + const nestedChildren = convertToNestedTags(child, currentPath, blockPrefix) + + const leafChildren: NestedTagChild[] = [] + const folders: NestedTag[] = [] + + for (const nestedChild of nestedChildren) { + if (nestedChild.nestedChildren || nestedChild.children) { + folders.push(nestedChild) + } else { + leafChildren.push({ + key: nestedChild.key, + display: nestedChild.display, + fullTag: nestedChild.fullTag!, + }) + } + } + + result.push({ + key: currentPath, + display: key, + parentTag, + children: leafChildren.length > 0 ? leafChildren : undefined, + nestedChildren: folders.length > 0 ? folders : undefined, + }) + } + } + + return result + } + + return convertToNestedTags(root, '', blockName) +} + const TagIcon: React.FC<{ icon: string | React.ComponentType<{ className?: string }> color: string @@ -319,62 +435,516 @@ const TagIcon: React.FC<{ ) /** - * Wrapper for PopoverBackButton that handles parent tag navigation. - * - * @remarks - * Extends the base PopoverBackButton to support selecting the parent tag - * when navigating back from a nested folder view. - * - * @param selectedIndex - Currently selected item index - * @param setSelectedIndex - Callback to update selection - * @param flatTagList - Flat list of all available tags - * @param nestedBlockTagGroups - Groups of nested block tags - * @param itemRefs - Refs to item DOM elements for scrolling - * @returns The back button component with parent tag support + * Props for the recursive NestedTagRenderer component */ -const TagDropdownBackButton: React.FC<{ +interface NestedTagRendererProps { + nestedTag: NestedTag + group: NestedBlockTagGroup + flatTagList: Array<{ tag: string; group?: BlockTagGroup }> selectedIndex: number setSelectedIndex: (index: number) => void - flatTagList: Array<{ tag: string; group?: BlockTagGroup }> - nestedBlockTagGroups: NestedBlockTagGroup[] - itemRefs: React.MutableRefObject> -}> = ({ selectedIndex, setSelectedIndex, flatTagList, nestedBlockTagGroups, itemRefs }) => { - const { currentFolder } = usePopoverContext() + handleTagSelect: (tag: string, blockGroup?: BlockTagGroup) => void + itemRefs: React.RefObject> + blocks: Record + getMergedSubBlocks: (blockId: string) => Record +} + +/** + * Props for FolderContents with nested navigation state + */ +interface FolderContentsProps extends NestedTagRendererProps { + /** Current nested path stack for navigation within this folder */ + nestedPath: NestedTag[] + /** Callback to navigate into a subfolder */ + onNavigateIn: (nestedTag: NestedTag) => void +} - // Find parent tag info for current folder - const parentTagInfo = useMemo(() => { - if (!currentFolder) return null +/** + * Renders the contents of a folder (leaf children + nested subfolder triggers). + * The parent tag is rendered as the first item in the list for selection. + */ +const FolderContentsInner: React.FC = ({ + group, + flatTagList, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + blocks, + getMergedSubBlocks, + nestedPath, + nestedTag, + onNavigateIn, +}) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() + const currentNestedTag = nestedPath.length > 0 ? nestedPath[nestedPath.length - 1] : nestedTag - for (const group of nestedBlockTagGroups) { - for (const nestedTag of group.nestedTags) { - const folderId = `${group.blockId}-${nestedTag.key}` - if (folderId === currentFolder && nestedTag.parentTag) { - const parentIdx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) - return parentIdx >= 0 ? { index: parentIdx } : null + const parentTagIndex = currentNestedTag.parentTag + ? flatTagList.findIndex((item) => item.tag === currentNestedTag.parentTag) + : -1 + + return ( + <> + {/* Render parent tag as the first selectable item */} + {currentNestedTag.parentTag && ( + = 0} + onMouseEnter={() => { + // Skip selection update during keyboard navigation to prevent scroll-triggered selection changes + if (isKeyboardNav) return + setKeyboardNav(false) + if (parentTagIndex >= 0) setSelectedIndex(parentTagIndex) + }} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(currentNestedTag.parentTag!, group) + }} + ref={(el) => { + if (el && parentTagIndex >= 0) { + itemRefs.current?.set(parentTagIndex, el) + } + }} + > + {currentNestedTag.display} + + )} + + {/* Render leaf children as PopoverItems */} + {currentNestedTag.children?.map((child) => { + const childGlobalIndex = flatTagList.findIndex((item) => item.tag === child.fullTag) + + const tagParts = child.fullTag.split('.') + const outputPath = tagParts.slice(1).join('.') + + let childType = '' + const block = Object.values(blocks).find((b) => b.id === group.blockId) + if (block) { + const blockConfig = getBlock(block.type) + const mergedSubBlocks = getMergedSubBlocks(group.blockId) + + childType = getOutputTypeForPath( + block, + blockConfig || null, + group.blockId, + outputPath, + mergedSubBlocks + ) } + + return ( + = 0} + onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (childGlobalIndex >= 0) setSelectedIndex(childGlobalIndex) + }} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(child.fullTag, group) + }} + ref={(el) => { + if (el && childGlobalIndex >= 0) { + itemRefs.current?.set(childGlobalIndex, el) + } + }} + > + {child.display} + {childType && childType !== 'any' && ( + + {childType} + + )} + + ) + })} + + {/* Render nested children as clickable folder items */} + {currentNestedTag.nestedChildren?.map((nestedChild) => { + const parentGlobalIndex = nestedChild.parentTag + ? flatTagList.findIndex((item) => item.tag === nestedChild.parentTag) + : -1 + + return ( + = 0} + onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (parentGlobalIndex >= 0) setSelectedIndex(parentGlobalIndex) + }} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + // Navigate into the subfolder on click + onNavigateIn(nestedChild) + }} + ref={(el) => { + if (el && parentGlobalIndex >= 0) { + itemRefs.current?.set(parentGlobalIndex, el) + } + }} + > + {nestedChild.display} + {'>'} + + ) + })} + + ) +} + +/** + * Wrapper component that uses shared nested navigation state from context. + * Handles registration of the base folder and navigation callbacks. + */ +const FolderContents: React.FC = (props) => { + const nestedNav = useNestedNavigation() + const { currentFolder } = usePopoverContext() + + const nestedPath = nestedNav?.nestedPath ?? [] + + useEffect(() => { + if (nestedNav && currentFolder) { + const folderId = `${props.group.blockId}-${props.nestedTag.key}` + if (currentFolder === folderId) { + nestedNav.registerFolder(folderId, props.nestedTag.display, props.nestedTag, props.group) } } - return null - }, [currentFolder, nestedBlockTagGroups, flatTagList]) + }, [currentFolder, nestedNav, props.group, props.nestedTag]) + + const handleNavigateIn = useCallback( + (nestedTag: NestedTag) => { + nestedNav?.navigateIn(nestedTag, props.group) + }, + [nestedNav, props.group] + ) + + return +} + +/** + * Recursively renders nested tags with PopoverFolder components. + * Handles arbitrary depth of nesting for deeply nested object structures. + */ +const NestedTagRenderer: React.FC = ({ + nestedTag, + group, + flatTagList, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + blocks, + getMergedSubBlocks, +}) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() + const hasChildren = nestedTag.children && nestedTag.children.length > 0 + const hasNestedChildren = nestedTag.nestedChildren && nestedTag.nestedChildren.length > 0 + + if (hasChildren || hasNestedChildren) { + const folderId = `${group.blockId}-${nestedTag.key}` + + const parentGlobalIndex = nestedTag.parentTag + ? flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) + : -1 + + return ( + = 0} + onSelect={() => { + if (nestedTag.parentTag) { + handleTagSelect(nestedTag.parentTag, group) + } + }} + onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (parentGlobalIndex >= 0) { + setSelectedIndex(parentGlobalIndex) + } + }} + ref={(el) => { + if (el && parentGlobalIndex >= 0) { + itemRefs.current?.set(parentGlobalIndex, el) + } + }} + > + + + ) + } - if (!parentTagInfo) { - return + // Leaf tag - render as a simple PopoverItem + const globalIndex = nestedTag.fullTag + ? flatTagList.findIndex((item) => item.tag === nestedTag.fullTag) + : -1 + + let tagDescription = '' + + // Handle loop/parallel contextual tags with special types + if ( + (group.blockType === 'loop' || group.blockType === 'parallel') && + !nestedTag.key.includes('.') + ) { + if (nestedTag.key === 'index') { + tagDescription = 'number' + } else if (nestedTag.key === 'currentItem') { + tagDescription = 'any' + } else if (nestedTag.key === 'items') { + tagDescription = 'array' + } + } else if (nestedTag.fullTag) { + const tagParts = nestedTag.fullTag.split('.') + const outputPath = tagParts.slice(1).join('.') + + const block = Object.values(blocks).find((b) => b.id === group.blockId) + if (block) { + const blockConfig = getBlock(block.type) + const mergedSubBlocks = getMergedSubBlocks(group.blockId) + + tagDescription = getOutputTypeForPath( + block, + blockConfig || null, + group.blockId, + outputPath, + mergedSubBlocks + ) + } } - const isActive = parentTagInfo.index === selectedIndex + return ( + = 0} + onMouseEnter={() => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (globalIndex >= 0) setSelectedIndex(globalIndex) + }} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + if (nestedTag.fullTag) { + handleTagSelect(nestedTag.fullTag, group) + } + }} + ref={(el) => { + if (el && globalIndex >= 0) { + itemRefs.current?.set(globalIndex, el) + } + }} + > + {nestedTag.display} + {tagDescription && tagDescription !== 'any' && ( + + {tagDescription} + + )} + + ) +} + +/** + * Hook to get mouse enter handler that respects keyboard navigation mode. + * Returns a handler that only updates selection if not in keyboard mode. + */ +const useKeyboardAwareMouseEnter = ( + setSelectedIndex: (index: number) => void +): ((index: number) => void) => { + const { isKeyboardNav, setKeyboardNav } = usePopoverContext() + + return useCallback( + (index: number) => { + if (isKeyboardNav) return + setKeyboardNav(false) + if (index >= 0) setSelectedIndex(index) + }, + [isKeyboardNav, setKeyboardNav, setSelectedIndex] + ) +} + +/** + * Wrapper for variable tag items that has access to popover context + */ +const VariableTagItem: React.FC<{ + tag: string + globalIndex: number + selectedIndex: number + setSelectedIndex: (index: number) => void + handleTagSelect: (tag: string) => void + itemRefs: React.RefObject> + variableInfo: { type: string; id: string } | null +}> = ({ + tag, + globalIndex, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + variableInfo, +}) => { + const handleMouseEnter = useKeyboardAwareMouseEnter(setSelectedIndex) return ( - { - if (el) { - itemRefs.current.set(parentTagInfo.index, el) + = 0} + onMouseEnter={() => handleMouseEnter(globalIndex)} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(tag) + }} + ref={(el) => { + if (el && globalIndex >= 0) { + itemRefs.current?.set(globalIndex, el) } }} - folderTitleActive={isActive} - onFolderTitleMouseEnter={() => { - setSelectedIndex(parentTagInfo.index) + > + + {tag.startsWith(TAG_PREFIXES.VARIABLE) ? tag.substring(TAG_PREFIXES.VARIABLE.length) : tag} + + {variableInfo && ( + + {variableInfo.type} + + )} + + ) +} + +/** + * Wrapper for block root tag items that has access to popover context + */ +const BlockRootTagItem: React.FC<{ + rootTag: string + rootTagGlobalIndex: number + selectedIndex: number + setSelectedIndex: (index: number) => void + handleTagSelect: (tag: string, group?: BlockTagGroup) => void + itemRefs: React.RefObject> + group: BlockTagGroup + tagIcon: string | React.ComponentType<{ className?: string }> + blockColor: string + blockName: string +}> = ({ + rootTag, + rootTagGlobalIndex, + selectedIndex, + setSelectedIndex, + handleTagSelect, + itemRefs, + group, + tagIcon, + blockColor, + blockName, +}) => { + const handleMouseEnter = useKeyboardAwareMouseEnter(setSelectedIndex) + + return ( + = 0} + onMouseEnter={() => handleMouseEnter(rootTagGlobalIndex)} + onMouseDown={(e) => { + e.preventDefault() + e.stopPropagation() + handleTagSelect(rootTag, group) }} - /> + ref={(el) => { + if (el && rootTagGlobalIndex >= 0) { + itemRefs.current?.set(rootTagGlobalIndex, el) + } + }} + > + + {blockName} + + ) +} + +/** + * Helper component to capture popover context for nested navigation + */ +const PopoverContextCapture: React.FC<{ + contextRef: React.RefObject<{ + openFolder: (id: string, title: string, onLoad?: () => void, onSelect?: () => void) => void + } | null> +}> = ({ contextRef }) => { + const { openFolder } = usePopoverContext() + useEffect(() => { + if (contextRef && 'current' in contextRef) { + ;(contextRef as { current: typeof contextRef.current }).current = { openFolder } + } + }, [openFolder, contextRef]) + return null +} + +/** + * Back button that handles nested navigation. + * When in nested folders, goes back one level at a time. + * At the root folder level, closes the folder. + */ +const TagDropdownBackButton: React.FC = () => { + const { isInFolder, closeFolder, colorScheme, size } = usePopoverContext() + const nestedNav = useNestedNavigation() + + if (!isInFolder) return null + + const handleBackClick = (e: React.MouseEvent) => { + e.stopPropagation() + // Try to navigate back in nested path first + if (nestedNav?.navigateBack()) { + // Successfully navigated back one level + return + } + // At root folder level, close the folder + closeFolder() + } + + // Just render the back button - the parent tag is rendered as the first item in FolderContentsInner + return ( +

+ + + + Back +
) } @@ -415,6 +985,16 @@ export const TagDropdown: React.FC = ({ const [selectedIndex, setSelectedIndex] = useState(0) const itemRefs = useRef>(new Map()) + const [nestedPath, setNestedPath] = useState([]) + const baseFolderRef = useRef<{ + id: string + title: string + baseTag: NestedTag + group: NestedBlockTagGroup + } | null>(null) + const handleTagSelectRef = useRef<((tag: string, group?: BlockTagGroup) => void) | null>(null) + const scrollAreaRef = useRef(null) + const { blocks, edges, loops, parallels } = useWorkflowStore( useShallow((state) => ({ blocks: state.blocks, @@ -620,7 +1200,6 @@ export const TagDropdown: React.FC = ({ if (dynamicOutputs.length > 0) { const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`) - // For self-reference, only show url and resumeEndpoint (not response format fields) blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url') || tag.endsWith('.resumeEndpoint')) : allTags @@ -635,7 +1214,7 @@ export const TagDropdown: React.FC = ({ const operationValue = mergedSubBlocks?.operation?.value ?? getSubBlockValue(activeSourceBlockId, 'operation') const toolOutputPaths = operationValue - ? getToolOutputPaths(blockConfig, operationValue) + ? getToolOutputPaths(blockConfig, operationValue, mergedSubBlocks) : [] if (toolOutputPaths.length > 0) { @@ -967,7 +1546,7 @@ export const TagDropdown: React.FC = ({ const operationValue = mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation') const toolOutputPaths = operationValue - ? getToolOutputPaths(blockConfig, operationValue) + ? getToolOutputPaths(blockConfig, operationValue, mergedSubBlocks) : [] if (toolOutputPaths.length > 0) { @@ -978,6 +1557,7 @@ export const TagDropdown: React.FC = ({ mergedSubBlocks, accessibleBlock.triggerMode ) + blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`) } } @@ -1070,72 +1650,41 @@ export const TagDropdown: React.FC = ({ const nestedBlockTagGroups: NestedBlockTagGroup[] = useMemo(() => { return filteredBlockTagGroups.map((group: BlockTagGroup) => { - const nestedTags: NestedTag[] = [] - const groupedTags: Record< - string, - Array<{ key: string; display: string; fullTag: string }> - > = {} - const directTags: Array<{ key: string; display: string; fullTag: string }> = [] + const normalizedBlockName = normalizeName(group.blockName) + + // Handle loop/parallel contextual tags (index, currentItem, items) + const directTags: NestedTag[] = [] + const tagsForTree: string[] = [] group.tags.forEach((tag: string) => { const tagParts = tag.split('.') - if (tagParts.length >= 3) { - const parent = tagParts[1] - const child = tagParts.slice(2).join('.') - if (!groupedTags[parent]) { - groupedTags[parent] = [] - } - groupedTags[parent].push({ - key: `${parent}.${child}`, - display: child, + // Loop/parallel contextual tags without block prefix + if ( + (group.blockType === 'loop' || group.blockType === 'parallel') && + tagParts.length === 1 + ) { + directTags.push({ + key: tag, + display: tag, fullTag: tag, }) - } else { - const path = tagParts.slice(1).join('.') - if ( - (group.blockType === 'loop' || group.blockType === 'parallel') && - tagParts.length === 1 - ) { - directTags.push({ - key: tag, - display: tag, - fullTag: tag, - }) - } else { - directTags.push({ - key: path || group.blockName, - display: path || group.blockName, - fullTag: tag, - }) - } - } - }) - - directTags.forEach((directTag) => { - nestedTags.push(directTag) - }) - - Object.entries(groupedTags).forEach(([parent, children]) => { - const firstChildTag = children[0]?.fullTag - if (firstChildTag) { - const tagParts = firstChildTag.split('.') - const parentTag = `${tagParts[0]}.${parent}` - nestedTags.push({ - key: parent, - display: parent, - parentTag: parentTag, - children: children, + } else if (tagParts.length === 2) { + // Direct property like blockname.property + directTags.push({ + key: tagParts[1], + display: tagParts[1], + fullTag: tag, }) } else { - nestedTags.push({ - key: parent, - display: parent, - children: children, - }) + // Nested property - add to tree builder + tagsForTree.push(tag) } }) + // Build recursive tree from nested tags + const nestedTags = [...directTags, ...buildNestedTagTree(tagsForTree, normalizedBlockName)] + return { ...group, nestedTags, @@ -1150,6 +1699,32 @@ export const TagDropdown: React.FC = ({ list.push({ tag }) }) + const flattenNestedTag = (nestedTag: NestedTag, group: BlockTagGroup, rootTag: string) => { + if (nestedTag.fullTag === rootTag) { + return + } + + if (nestedTag.parentTag) { + list.push({ tag: nestedTag.parentTag, group }) + } + + if (nestedTag.fullTag && !nestedTag.children && !nestedTag.nestedChildren) { + list.push({ tag: nestedTag.fullTag, group }) + } + + if (nestedTag.children) { + nestedTag.children.forEach((child) => { + list.push({ tag: child.fullTag, group }) + }) + } + + if (nestedTag.nestedChildren) { + nestedTag.nestedChildren.forEach((nestedChild) => { + flattenNestedTag(nestedChild, group, rootTag) + }) + } + } + nestedBlockTagGroups.forEach((group) => { const normalizedBlockName = normalizeName(group.blockName) const rootTagFromTags = group.tags.find((tag) => tag === normalizedBlockName) @@ -1158,21 +1733,7 @@ export const TagDropdown: React.FC = ({ list.push({ tag: rootTag, group }) group.nestedTags.forEach((nestedTag) => { - if (nestedTag.fullTag === rootTag) { - return - } - - if (nestedTag.parentTag) { - list.push({ tag: nestedTag.parentTag, group }) - } - if (nestedTag.fullTag) { - list.push({ tag: nestedTag.fullTag, group }) - } - if (nestedTag.children) { - nestedTag.children.forEach((child) => { - list.push({ tag: child.fullTag, group }) - }) - } + flattenNestedTag(nestedTag, group, rootTag) }) }) @@ -1185,7 +1746,7 @@ export const TagDropdown: React.FC = ({ const element = itemRefs.current.get(selectedIndex) if (element) { element.scrollIntoView({ - behavior: 'smooth', + behavior: 'auto', block: 'nearest', }) } @@ -1281,6 +1842,90 @@ export const TagDropdown: React.FC = ({ [inputValue, cursorPosition, workflowVariables, onSelect, onClose, getMergedSubBlocks] ) + handleTagSelectRef.current = handleTagSelect + + const popoverContextRef = useRef<{ + openFolder: (id: string, title: string, onLoad?: () => void, onSelect?: () => void) => void + } | null>(null) + + const nestedNavigationValue = useMemo( + () => ({ + nestedPath, + navigateIn: (tag: NestedTag, group: NestedBlockTagGroup) => { + const baseFolder = baseFolderRef.current + if (!baseFolder || !popoverContextRef.current) return + + setNestedPath((prev) => [...prev, tag]) + + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = 0 + } + + const selectionCallback = () => { + if (tag.parentTag && handleTagSelectRef.current) { + handleTagSelectRef.current(tag.parentTag, group) + } + } + popoverContextRef.current.openFolder( + baseFolder.id, + tag.display, + undefined, + selectionCallback + ) + }, + navigateBack: () => { + const baseFolder = baseFolderRef.current + if (!baseFolder || !popoverContextRef.current) return false + if (nestedPath.length === 0) return false + + const newPath = nestedPath.slice(0, -1) + setNestedPath(newPath) + + if (newPath.length === 0) { + const selectionCallback = () => { + if (baseFolder.baseTag.parentTag && handleTagSelectRef.current) { + handleTagSelectRef.current(baseFolder.baseTag.parentTag, baseFolder.group) + } + } + popoverContextRef.current.openFolder( + baseFolder.id, + baseFolder.title, + undefined, + selectionCallback + ) + } else { + const parentTag = newPath[newPath.length - 1] + const selectionCallback = () => { + if (parentTag.parentTag && handleTagSelectRef.current) { + handleTagSelectRef.current(parentTag.parentTag, baseFolder.group) + } + } + popoverContextRef.current.openFolder( + baseFolder.id, + parentTag.display, + undefined, + selectionCallback + ) + } + return true + }, + registerFolder: (folderId, folderTitle, baseTag, group) => { + baseFolderRef.current = { id: folderId, title: folderTitle, baseTag, group } + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = 0 + } + }, + }), + [nestedPath] + ) + + useEffect(() => { + if (!visible) { + setNestedPath([]) + baseFolderRef.current = null + } + }, [visible]) + useEffect(() => setSelectedIndex(0), [searchTerm]) useEffect(() => { @@ -1322,316 +1967,145 @@ export const TagDropdown: React.FC = ({ } return ( - !open && onClose?.()} colorScheme='inverted'> - -
- - - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - > - + !open && onClose?.()} colorScheme='inverted'> + + +
+ + - - {flatTagList.length === 0 ? ( -
- No matching tags found -
- ) : ( - <> - {variableTags.length > 0 && ( - <> - -
- - Variables -
-
- {variableTags.map((tag: string) => { - const variableInfo = variableInfoMap?.[tag] || null - const globalIndex = flatTagList.findIndex((item) => item.tag === tag) - - return ( - = 0} - onMouseEnter={() => { - if (globalIndex >= 0) setSelectedIndex(globalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(tag) - }} - ref={(el) => { - if (el && globalIndex >= 0) { - itemRefs.current.set(globalIndex, el) - } - }} - > - - {tag.startsWith(TAG_PREFIXES.VARIABLE) - ? tag.substring(TAG_PREFIXES.VARIABLE.length) - : tag} - - {variableInfo && ( - - {variableInfo.type} - - )} - - ) - })} - {nestedBlockTagGroups.length > 0 && } - - )} - - {nestedBlockTagGroups.map((group: NestedBlockTagGroup, groupIndex: number) => { - const blockConfig = getBlock(group.blockType) - let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT - - if (group.blockType === 'loop') { - blockColor = BLOCK_COLORS.LOOP - } else if (group.blockType === 'parallel') { - blockColor = BLOCK_COLORS.PARALLEL - } - - let tagIcon: string | React.ComponentType<{ className?: string }> = group.blockName - .charAt(0) - .toUpperCase() - if (blockConfig?.icon) { - tagIcon = blockConfig.icon - } else if (group.blockType === 'loop') { - tagIcon = RepeatIcon - } else if (group.blockType === 'parallel') { - tagIcon = SplitIcon - } - - const normalizedBlockName = normalizeName(group.blockName) - const rootTagFromTags = group.tags.find((tag) => tag === normalizedBlockName) - const rootTag = rootTagFromTags || normalizedBlockName - - const rootTagGlobalIndex = flatTagList.findIndex((item) => item.tag === rootTag) - - return ( -
- = 0} - onMouseEnter={() => { - if (rootTagGlobalIndex >= 0) setSelectedIndex(rootTagGlobalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(rootTag, group) - }} - ref={(el) => { - if (el && rootTagGlobalIndex >= 0) { - itemRefs.current.set(rootTagGlobalIndex, el) - } - }} - > - - {group.blockName} - - {group.nestedTags.map((nestedTag) => { - if (nestedTag.fullTag === rootTag) { - return null - } - - const hasChildren = nestedTag.children && nestedTag.children.length > 0 - - if (hasChildren) { - const folderId = `${group.blockId}-${nestedTag.key}` - - const parentGlobalIndex = nestedTag.parentTag - ? flatTagList.findIndex((item) => item.tag === nestedTag.parentTag) - : -1 - - return ( - = 0} - onSelect={() => { - if (nestedTag.parentTag) { - handleTagSelect(nestedTag.parentTag, group) - } - }} - onMouseEnter={() => { - if (parentGlobalIndex >= 0) { - setSelectedIndex(parentGlobalIndex) - } - }} - ref={(el) => { - if (el && parentGlobalIndex >= 0) { - itemRefs.current.set(parentGlobalIndex, el) - } - }} - > - {nestedTag.children!.map((child) => { - const childGlobalIndex = flatTagList.findIndex( - (item) => item.tag === child.fullTag - ) - - const tagParts = child.fullTag.split('.') - const outputPath = tagParts.slice(1).join('.') - - let childType = '' - const block = Object.values(blocks).find( - (b) => b.id === group.blockId - ) - if (block) { - const blockConfig = getBlock(block.type) - const mergedSubBlocks = getMergedSubBlocks(group.blockId) - - childType = getOutputTypeForPath( - block, - blockConfig || null, - group.blockId, - outputPath, - mergedSubBlocks - ) - } - - return ( - = 0 - } - onMouseEnter={() => { - if (childGlobalIndex >= 0) setSelectedIndex(childGlobalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - handleTagSelect(child.fullTag, group) - }} - ref={(el) => { - if (el && childGlobalIndex >= 0) { - itemRefs.current.set(childGlobalIndex, el) - } - }} - > - {child.display} - {childType && childType !== 'any' && ( - - {childType} - - )} - - ) - })} - - ) - } - - const globalIndex = nestedTag.fullTag - ? flatTagList.findIndex((item) => item.tag === nestedTag.fullTag) - : -1 - - let tagDescription = '' - - if ( - (group.blockType === 'loop' || group.blockType === 'parallel') && - !nestedTag.key.includes('.') - ) { - if (nestedTag.key === 'index') { - tagDescription = 'number' - } else if (nestedTag.key === 'currentItem') { - tagDescription = 'any' - } else if (nestedTag.key === 'items') { - tagDescription = 'array' - } - } else if (nestedTag.fullTag) { - const tagParts = nestedTag.fullTag.split('.') - const outputPath = tagParts.slice(1).join('.') - - const block = Object.values(blocks).find((b) => b.id === group.blockId) - if (block) { - const blockConfig = getBlock(block.type) - const mergedSubBlocks = getMergedSubBlocks(group.blockId) - - tagDescription = getOutputTypeForPath( - block, - blockConfig || null, - group.blockId, - outputPath, - mergedSubBlocks - ) - } - } + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > + + + {flatTagList.length === 0 ? ( +
+ No matching tags found +
+ ) : ( + <> + {variableTags.length > 0 && ( + <> + +
+ + Variables +
+
+ {variableTags.map((tag: string) => { + const variableInfo = variableInfoMap?.[tag] || null + const globalIndex = flatTagList.findIndex((item) => item.tag === tag) return ( - = 0} - onMouseEnter={() => { - if (globalIndex >= 0) setSelectedIndex(globalIndex) - }} - onMouseDown={(e) => { - e.preventDefault() - e.stopPropagation() - if (nestedTag.fullTag) { - handleTagSelect(nestedTag.fullTag, group) - } - }} - ref={(el) => { - if (el && globalIndex >= 0) { - itemRefs.current.set(globalIndex, el) - } - }} - > - {nestedTag.display} - {tagDescription && tagDescription !== 'any' && ( - - {tagDescription} - - )} - + ) })} - {groupIndex < nestedBlockTagGroups.length - 1 && } -
- ) - })} - - )} -
- - + {nestedBlockTagGroups.length > 0 && } + + )} + + {nestedBlockTagGroups.map((group: NestedBlockTagGroup, groupIndex: number) => { + const blockConfig = getBlock(group.blockType) + let blockColor = blockConfig?.bgColor || BLOCK_COLORS.DEFAULT + + if (group.blockType === 'loop') { + blockColor = BLOCK_COLORS.LOOP + } else if (group.blockType === 'parallel') { + blockColor = BLOCK_COLORS.PARALLEL + } + + let tagIcon: string | React.ComponentType<{ className?: string }> = + group.blockName.charAt(0).toUpperCase() + if (blockConfig?.icon) { + tagIcon = blockConfig.icon + } else if (group.blockType === 'loop') { + tagIcon = RepeatIcon + } else if (group.blockType === 'parallel') { + tagIcon = SplitIcon + } + + const normalizedBlockName = normalizeName(group.blockName) + const rootTagFromTags = group.tags.find((tag) => tag === normalizedBlockName) + const rootTag = rootTagFromTags || normalizedBlockName + + const rootTagGlobalIndex = flatTagList.findIndex((item) => item.tag === rootTag) + + return ( +
+ + {group.nestedTags.map((nestedTag) => { + if (nestedTag.fullTag === rootTag) { + return null + } + + return ( + + ) + })} + {groupIndex < nestedBlockTagGroups.length - 1 && } +
+ ) + })} + + )} + + + + ) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts index 64f256babd..d17fc28745 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts @@ -10,14 +10,27 @@ export interface BlockTagGroup { } /** - * Nested tag structure for hierarchical display + * Child tag within a nested structure + */ +export interface NestedTagChild { + key: string + display: string + fullTag: string +} + +/** + * Nested tag structure for hierarchical display. + * Supports recursive nesting for deeply nested object structures. */ export interface NestedTag { key: string display: string fullTag?: string - parentTag?: string // Tag for the parent object when it has children - children?: Array<{ key: string; display: string; fullTag: string }> + parentTag?: string + /** Leaf children (no further nesting) */ + children?: NestedTagChild[] + /** Recursively nested folders */ + nestedChildren?: NestedTag[] } /** diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index a47b0f97de..2ea7cb5572 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -169,6 +169,8 @@ const getPreviewValue = ( * @param isValidJson - Whether the JSON content is valid (for code blocks) * @param subBlockValues - Current values of all subblocks for evaluating conditional requirements * @param wandState - Optional state and handlers for the AI wand feature + * @param canonicalToggle - Optional canonical toggle metadata and handlers + * @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled * @returns The label JSX element, or `null` for switch types or when no title is defined */ const renderLabel = ( @@ -193,7 +195,8 @@ const renderLabel = ( mode: 'basic' | 'advanced' disabled?: boolean onToggle?: () => void - } + }, + canonicalToggleIsDisabled?: boolean ): JSX.Element | null => { if (config.type === 'switch') return null if (!config.title) return null @@ -201,7 +204,7 @@ const renderLabel = ( const required = isFieldRequired(config, subBlockValues) const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled const showCanonicalToggle = !!canonicalToggle && !wandState?.isPreview - const canonicalToggleDisabled = wandState?.disabled || canonicalToggle?.disabled + const canonicalToggleDisabledResolved = canonicalToggleIsDisabled ?? canonicalToggle?.disabled return (
@@ -286,7 +289,7 @@ const renderLabel = ( type='button' className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50' onClick={canonicalToggle?.onToggle} - disabled={canonicalToggleDisabled} + disabled={canonicalToggleDisabledResolved} aria-label={canonicalToggle?.mode === 'advanced' ? 'Use selector' : 'Enter manual ID'} > diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 30ab836172..d9fa9a2062 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -7,8 +7,8 @@ import { useShallow } from 'zustand/react/shallow' import { Button, Tooltip } from '@/components/emcn' import { buildCanonicalIndex, + evaluateSubBlockCondition, hasAdvancedValues, - hasStandaloneAdvancedFields, isCanonicalPair, resolveCanonicalMode, } from '@/lib/workflows/subblocks/visibility' @@ -131,10 +131,24 @@ export function Editor() { ) const displayAdvancedOptions = advancedMode || advancedValuesPresent - const hasAdvancedOnlyFields = useMemo( - () => hasStandaloneAdvancedFields(subBlocksForCanonical, canonicalIndex), - [subBlocksForCanonical, canonicalIndex] - ) + const hasAdvancedOnlyFields = useMemo(() => { + for (const subBlock of subBlocksForCanonical) { + // Must be standalone advanced (mode: 'advanced' without canonicalParamId) + if (subBlock.mode !== 'advanced') continue + if (canonicalIndex.canonicalIdBySubBlockId[subBlock.id]) continue + + // Check condition - skip if condition not met for current values + if ( + subBlock.condition && + !evaluateSubBlockCondition(subBlock.condition, blockSubBlockValues) + ) { + continue + } + + return true + } + return false + }, [subBlocksForCanonical, canonicalIndex.canonicalIdBySubBlockId, blockSubBlockValues]) // Get subblock layout using custom hook const { subBlocks, stateToUse: subBlockState } = useEditorSubblockLayout( @@ -480,7 +494,9 @@ export function Editor() { onClick={handleToggleAdvancedMode} className='flex items-center gap-[6px] whitespace-nowrap font-medium text-[13px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]' > - {displayAdvancedOptions ? 'Hide advanced fields' : 'Show advanced fields'} + {displayAdvancedOptions + ? 'Hide additional fields' + : 'Show additional fields'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 2f7447cf79..570f13900d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -50,10 +50,10 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks' import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { getBlock } from '@/blocks' +import { useShowTrainingControls } from '@/hooks/queries/general-settings' import { useCodeViewerFeatures } from '@/hooks/use-code-viewer' import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants' import { useCopilotTrainingStore } from '@/stores/copilot-training/store' -import { useGeneralStore } from '@/stores/settings/general' import type { ConsoleEntry } from '@/stores/terminal' import { useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -830,7 +830,7 @@ export const Terminal = memo(function Terminal() { const [outputOptionsOpen, setOutputOptionsOpen] = useState(false) const [isTrainingEnvEnabled, setIsTrainingEnvEnabled] = useState(false) - const showTrainingControls = useGeneralStore((state) => state.showTrainingControls) + const showTrainingControls = useShowTrainingControls() const { isTraining, toggleModal: toggleTrainingModal, stopTraining } = useCopilotTrainingStore() const [isPlaygroundEnabled, setIsPlaygroundEnabled] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx index 0e243faae9..e646d61dcf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx @@ -22,11 +22,10 @@ import { import { useSession } from '@/lib/auth/auth-client' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils' -import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings' +import { useShowActionBar, useUpdateGeneralSetting } from '@/hooks/queries/general-settings' import { useCanvasViewport } from '@/hooks/use-canvas-viewport' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useCanvasModeStore } from '@/stores/canvas-mode' -import { useGeneralStore } from '@/stores/settings/general' import { useTerminalStore } from '@/stores/terminal' import { useUndoRedoStore } from '@/stores/undo-redo' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -41,7 +40,7 @@ export const WorkflowControls = memo(function WorkflowControls() { const { fitViewToBounds } = useCanvasViewport(reactFlowInstance) const { mode, setMode } = useCanvasModeStore() const { undo, redo } = useCollaborativeWorkflow() - const showWorkflowControls = useGeneralStore((s) => s.showActionBar) + const showWorkflowControls = useShowActionBar() const updateSetting = useUpdateGeneralSetting() const isTerminalResizing = useTerminalStore((state) => state.isResizing) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts index fec22a0970..5dade163b5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts @@ -8,13 +8,14 @@ type MenuType = 'block' | 'pane' | null interface UseCanvasContextMenuProps { blocks: Record getNodes: () => Node[] + setNodes: (updater: (nodes: Node[]) => Node[]) => void } /** * Hook for managing workflow canvas context menus. * Handles right-click events, menu state, click-outside detection, and block info extraction. */ -export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuProps) { +export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasContextMenuProps) { const [activeMenu, setActiveMenu] = useState(null) const [position, setPosition] = useState({ x: 0, y: 0 }) const [selectedBlocks, setSelectedBlocks] = useState([]) @@ -44,14 +45,26 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP event.preventDefault() event.stopPropagation() + const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey + setNodes((nodes) => + nodes.map((n) => ({ + ...n, + selected: isMultiSelect ? (n.id === node.id ? true : n.selected) : n.id === node.id, + })) + ) + const selectedNodes = getNodes().filter((n) => n.selected) - const nodesToUse = selectedNodes.some((n) => n.id === node.id) ? selectedNodes : [node] + const nodesToUse = isMultiSelect + ? selectedNodes.some((n) => n.id === node.id) + ? selectedNodes + : [...selectedNodes, node] + : [node] setPosition({ x: event.clientX, y: event.clientY }) setSelectedBlocks(nodesToBlockInfos(nodesToUse)) setActiveMenu('block') }, - [getNodes, nodesToBlockInfos] + [getNodes, nodesToBlockInfos, setNodes] ) const handlePaneContextMenu = useCallback((event: React.MouseEvent) => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index a32bbb96aa..b2d2a4caa2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -63,6 +63,7 @@ import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' +import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings' import { useCanvasViewport } from '@/hooks/use-canvas-viewport' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -74,7 +75,6 @@ import { useExecutionStore } from '@/stores/execution' import { useSearchModalStore } from '@/stores/modals/search/store' import { useNotificationStore } from '@/stores/notifications' import { useCopilotStore, usePanelEditorStore } from '@/stores/panel' -import { useGeneralStore } from '@/stores/settings/general' import { useUndoRedoStore } from '@/stores/undo-redo' import { useVariablesStore } from '@/stores/variables/store' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' @@ -234,6 +234,7 @@ const WorkflowContent = React.memo(() => { const [potentialParentId, setPotentialParentId] = useState(null) const [selectedEdges, setSelectedEdges] = useState(new Map()) const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false) + const selectedIdsRef = useRef(null) const canvasMode = useCanvasModeStore((state) => state.mode) const isHandMode = canvasMode === 'hand' const { handleCanvasMouseDown, selectionProps } = useShiftSelectionLock({ isHandMode }) @@ -308,9 +309,15 @@ const WorkflowContent = React.memo(() => { const showTrainingModal = useCopilotTrainingStore((state) => state.showModal) - const snapToGridSize = useGeneralStore((state) => state.snapToGridSize) + const snapToGridSize = useSnapToGridSize() const snapToGrid = snapToGridSize > 0 + const isAutoConnectEnabled = useAutoConnect() + const autoConnectRef = useRef(isAutoConnectEnabled) + useEffect(() => { + autoConnectRef.current = isAutoConnectEnabled + }, [isAutoConnectEnabled]) + // Panel open states for context menu const isVariablesOpen = useVariablesStore((state) => state.isOpen) const isChatOpen = useChatStore((state) => state.isChatOpen) @@ -858,7 +865,7 @@ const WorkflowContent = React.memo(() => { handlePaneContextMenu, handleSelectionContextMenu, closeMenu: closeContextMenu, - } = useCanvasContextMenu({ blocks, getNodes }) + } = useCanvasContextMenu({ blocks, getNodes, setNodes }) const handleContextCopy = useCallback(() => { const blockIds = contextMenuBlocks.map((b) => b.id) @@ -1217,8 +1224,7 @@ const WorkflowContent = React.memo(() => { containerId?: string } ): Edge | undefined => { - const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled - if (!isAutoConnectEnabled) return undefined + if (!autoConnectRef.current) return undefined // Don't auto-connect starter or annotation-only blocks if (options.blockType === 'starter' || isAnnotationOnlyBlock(options.blockType)) { @@ -2148,11 +2154,25 @@ const WorkflowContent = React.memo(() => { /** Handles node changes - applies changes and resolves parent-child selection conflicts. */ const onNodesChange = useCallback( (changes: NodeChange[]) => { + selectedIdsRef.current = null setDisplayNodes((nds) => { const updated = applyNodeChanges(changes, nds) const hasSelectionChange = changes.some((c) => c.type === 'select') - return hasSelectionChange ? resolveParentChildSelectionConflicts(updated, blocks) : updated + if (!hasSelectionChange) return updated + const resolved = resolveParentChildSelectionConflicts(updated, blocks) + selectedIdsRef.current = resolved.filter((node) => node.selected).map((node) => node.id) + return resolved }) + const selectedIds = selectedIdsRef.current as string[] | null + if (selectedIds !== null) { + const { currentBlockId, clearCurrentBlock, setCurrentBlockId } = + usePanelEditorStore.getState() + if (selectedIds.length === 1 && selectedIds[0] !== currentBlockId) { + setCurrentBlockId(selectedIds[0]) + } else if (selectedIds.length === 0 && currentBlockId) { + clearCurrentBlock() + } + } }, [blocks] ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx index 01fdab5a27..ee8ec7499d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/subscription/subscription.tsx @@ -32,11 +32,13 @@ import { UsageLimit, type UsageLimitRef, } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/usage-limit' -import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings' +import { + useBillingUsageNotifications, + useUpdateGeneralSetting, +} from '@/hooks/queries/general-settings' import { useOrganizationBilling, useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData, useUsageLimitData } from '@/hooks/queries/subscription' import { useUpdateWorkspaceSettings, useWorkspaceSettings } from '@/hooks/queries/workspace' -import { useGeneralStore } from '@/stores/settings/general' const CONSTANTS = { UPGRADE_ERROR_TIMEOUT: 3000, // 3 seconds @@ -627,7 +629,7 @@ export function Subscription() { } function BillingUsageNotificationsToggle() { - const enabled = useGeneralStore((s) => s.isBillingUsageNotificationsEnabled) + const enabled = useBillingUsageNotifications() const updateSetting = useUpdateGeneralSetting() const isLoading = updateSetting.isPending diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx index 1c1984d4a1..b03138de55 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Check } from 'lucide-react' import { Button, @@ -11,10 +11,110 @@ import { PopoverDivider, PopoverFolder, PopoverItem, + usePopoverContext, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { WORKFLOW_COLORS } from '@/lib/workflows/colors' +const GRID_COLUMNS = 6 + +/** + * Color grid with keyboard navigation support. + * Uses roving tabindex pattern for accessibility. + */ +function ColorGrid({ + hexInput, + setHexInput, +}: { + hexInput: string + setHexInput: (color: string) => void +}) { + const { isInFolder } = usePopoverContext() + const [focusedIndex, setFocusedIndex] = useState(-1) + const gridRef = useRef(null) + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]) + + useEffect(() => { + if (isInFolder && gridRef.current) { + const selectedIndex = WORKFLOW_COLORS.findIndex( + ({ color }) => color.toLowerCase() === hexInput.toLowerCase() + ) + const initialIndex = selectedIndex >= 0 ? selectedIndex : 0 + setFocusedIndex(initialIndex) + setTimeout(() => { + buttonRefs.current[initialIndex]?.focus() + }, 50) + } + }, [isInFolder, hexInput]) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent, index: number) => { + const totalItems = WORKFLOW_COLORS.length + let newIndex = index + + switch (e.key) { + case 'ArrowRight': + e.preventDefault() + newIndex = index + 1 < totalItems ? index + 1 : index + break + case 'ArrowLeft': + e.preventDefault() + newIndex = index - 1 >= 0 ? index - 1 : index + break + case 'ArrowDown': + e.preventDefault() + newIndex = index + GRID_COLUMNS < totalItems ? index + GRID_COLUMNS : index + break + case 'ArrowUp': + e.preventDefault() + newIndex = index - GRID_COLUMNS >= 0 ? index - GRID_COLUMNS : index + break + case 'Enter': + case ' ': + e.preventDefault() + setHexInput(WORKFLOW_COLORS[index].color) + return + default: + return + } + + if (newIndex !== index) { + setFocusedIndex(newIndex) + buttonRefs.current[newIndex]?.focus() + } + }, + [setHexInput] + ) + + return ( +
+ {WORKFLOW_COLORS.map(({ color, name }, index) => ( +
+ ) +} + /** * Validates a hex color string. * Accepts 3 or 6 character hex codes with or without #. @@ -349,25 +449,8 @@ export function ContextMenu({ className={disableColorChange ? 'pointer-events-none opacity-50' : ''} >
- {/* Preset colors */} -
- {WORKFLOW_COLORS.map(({ color, name }) => ( -
+ {/* Preset colors with keyboard navigation */} + {/* Hex input */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx index 59d17232a2..597d0e6cde 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal.tsx @@ -97,6 +97,15 @@ export function DeleteModal({ return 'Are you sure you want to delete this folder? This will permanently remove all associated workflows, logs, and knowledge bases.' } + if (isSingle && displayNames.length > 0) { + return ( + <> + Are you sure you want to delete{' '} + {displayNames[0]}? This + will permanently remove all associated workflows, folders, logs, and knowledge bases. + + ) + } return 'Are you sure you want to delete this workspace? This will permanently remove all associated workflows, folders, logs, and knowledge bases.' } diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 65ada43166..db010d696b 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -308,14 +308,30 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, condition: { field: 'operation', value: 'update' }, }, - // Move Event Fields + // Move Event Fields - Destination calendar selector (basic mode) { - id: 'destinationCalendarId', + id: 'destinationCalendar', + title: 'Destination Calendar', + type: 'file-selector', + canonicalParamId: 'destinationCalendarId', + serviceId: 'google-calendar', + requiredScopes: ['https://www.googleapis.com/auth/calendar'], + placeholder: 'Select destination calendar', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + mode: 'basic', + }, + // Move Event Fields - Manual destination calendar ID (advanced mode) + { + id: 'manualDestinationCalendarId', title: 'Destination Calendar ID', type: 'short-input', + canonicalParamId: 'destinationCalendarId', placeholder: 'destination@group.calendar.google.com', condition: { field: 'operation', value: 'move' }, required: true, + mode: 'advanced', }, // Instances Fields @@ -502,17 +518,31 @@ Return ONLY the natural language event text - no explanations.`, replaceExisting, calendarId, manualCalendarId, + destinationCalendar, + manualDestinationCalendarId, ...rest } = params // Handle calendar ID (selector or manual) const effectiveCalendarId = (calendarId || manualCalendarId || '').trim() + // Handle destination calendar ID for move operation (selector or manual) + const effectiveDestinationCalendarId = ( + destinationCalendar || + manualDestinationCalendarId || + '' + ).trim() + const processedParams: Record = { ...rest, calendarId: effectiveCalendarId || 'primary', } + // Add destination calendar ID for move operation + if (operation === 'move' && effectiveDestinationCalendarId) { + processedParams.destinationCalendarId = effectiveDestinationCalendarId + } + // Convert comma-separated attendees string to array, only if it has content if (attendees && typeof attendees === 'string' && attendees.trim().length > 0) { const attendeeList = attendees @@ -579,7 +609,8 @@ Return ONLY the natural language event text - no explanations.`, eventId: { type: 'string', description: 'Event identifier' }, // Move operation inputs - destinationCalendarId: { type: 'string', description: 'Destination calendar ID' }, + destinationCalendar: { type: 'string', description: 'Destination calendar selector' }, + manualDestinationCalendarId: { type: 'string', description: 'Manual destination calendar ID' }, // List Calendars operation inputs minAccessRole: { type: 'string', description: 'Minimum access role filter' }, diff --git a/apps/sim/blocks/blocks/google_drive.ts b/apps/sim/blocks/blocks/google_drive.ts index 23232db528..209bd12f91 100644 --- a/apps/sim/blocks/blocks/google_drive.ts +++ b/apps/sim/blocks/blocks/google_drive.ts @@ -30,7 +30,6 @@ export const GoogleDriveBlock: BlockConfig = { { 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' }, @@ -524,16 +523,6 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, 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', @@ -745,7 +734,6 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr 'google_drive_copy', 'google_drive_update', 'google_drive_trash', - 'google_drive_untrash', 'google_drive_delete', 'google_drive_share', 'google_drive_unshare', @@ -772,8 +760,6 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr return 'google_drive_update' case 'trash': return 'google_drive_trash' - case 'untrash': - return 'google_drive_untrash' case 'delete': return 'google_drive_delete' case 'share': @@ -875,12 +861,22 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr permissionId: { type: 'string', description: 'Permission ID to remove' }, }, outputs: { - file: { type: 'json', description: 'File metadata' }, + file: { type: 'json', description: 'File metadata or downloaded file data' }, files: { type: 'json', description: 'List of files' }, + metadata: { type: 'json', description: 'Complete file metadata (from download)' }, + content: { type: 'string', description: 'File content as text' }, + nextPageToken: { type: 'string', description: 'Token for fetching the next page of results' }, 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' }, + canCreateDrives: { type: 'boolean', description: 'Whether user can create shared drives' }, + importFormats: { type: 'json', description: 'Map of MIME types that can be imported' }, + exportFormats: { + type: 'json', + description: 'Map of Google Workspace MIME types and export formats', + }, + maxUploadSize: { type: 'string', description: 'Maximum upload size in bytes' }, deleted: { type: 'boolean', description: 'Whether file was deleted' }, removed: { type: 'boolean', description: 'Whether permission was removed' }, }, diff --git a/apps/sim/blocks/blocks/google_forms.ts b/apps/sim/blocks/blocks/google_forms.ts index 38f8d384c1..0dee689774 100644 --- a/apps/sim/blocks/blocks/google_forms.ts +++ b/apps/sim/blocks/blocks/google_forms.ts @@ -39,19 +39,40 @@ export const GoogleFormsBlock: BlockConfig = { requiredScopes: [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/drive', '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 + // Form selector (basic mode) { id: 'formId', + title: 'Select Form', + type: 'file-selector', + canonicalParamId: 'formId', + serviceId: 'google-forms', + requiredScopes: [], + mimeType: 'application/vnd.google-apps.form', + placeholder: 'Select a form', + dependsOn: ['credential'], + mode: 'basic', + condition: { + field: 'operation', + value: 'create_form', + not: true, + }, + }, + // Manual form ID input (advanced mode) + { + id: 'manualFormId', title: 'Form ID', type: 'short-input', + canonicalParamId: 'formId', required: true, placeholder: 'Enter the Google Form ID', dependsOn: ['credential'], + mode: 'advanced', condition: { field: 'operation', value: 'create_form', @@ -214,6 +235,7 @@ Example for "Add a required multiple choice question about favorite color": credential, operation, formId, + manualFormId, responseId, pageSize, title, @@ -230,7 +252,7 @@ Example for "Add a required multiple choice question about favorite color": } = params const baseParams = { ...rest, credential } - const effectiveFormId = formId ? String(formId).trim() : undefined + const effectiveFormId = (formId || manualFormId || '').toString().trim() || undefined switch (operation) { case 'get_responses': @@ -299,7 +321,8 @@ Example for "Add a required multiple choice question about favorite color": inputs: { operation: { type: 'string', description: 'Operation to perform' }, credential: { type: 'string', description: 'Google OAuth credential' }, - formId: { type: 'string', description: 'Google Form ID' }, + formId: { type: 'string', description: 'Google Form ID (from selector)' }, + manualFormId: { type: 'string', description: 'Google Form ID (manual entry)' }, responseId: { type: 'string', description: 'Specific response ID' }, pageSize: { type: 'string', description: 'Max responses to retrieve' }, title: { type: 'string', description: 'Form title for creation' }, @@ -314,13 +337,132 @@ Example for "Add a required multiple choice question about favorite color": 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' }, + responses: { + type: 'json', + description: 'Array of form responses', + condition: { + field: 'operation', + value: 'get_responses', + and: { field: 'responseId', value: ['', undefined, null] }, + }, + }, + response: { + type: 'json', + description: 'Single form response', + condition: { + field: 'operation', + value: 'get_responses', + and: { field: 'responseId', value: ['', undefined, null], not: true }, + }, + }, + // Get Form outputs + formId: { + type: 'string', + description: 'Form ID', + condition: { field: 'operation', value: ['get_form', 'create_form', 'set_publish_settings'] }, + }, + title: { + type: 'string', + description: 'Form title', + condition: { field: 'operation', value: ['get_form', 'create_form'] }, + }, + description: { + type: 'string', + description: 'Form description', + condition: { field: 'operation', value: 'get_form' }, + }, + documentTitle: { + type: 'string', + description: 'Document title in Drive', + condition: { field: 'operation', value: ['get_form', 'create_form'] }, + }, + responderUri: { + type: 'string', + description: 'Form responder URL', + condition: { field: 'operation', value: ['get_form', 'create_form'] }, + }, + linkedSheetId: { + type: 'string', + description: 'Linked Google Sheet ID', + condition: { field: 'operation', value: 'get_form' }, + }, + revisionId: { + type: 'string', + description: 'Form revision ID', + condition: { field: 'operation', value: ['get_form', 'create_form'] }, + }, + items: { + type: 'json', + description: 'Form items (questions, sections, etc.)', + condition: { field: 'operation', value: 'get_form' }, + }, + settings: { + type: 'json', + description: 'Form settings', + condition: { field: 'operation', value: 'get_form' }, + }, + publishSettings: { + type: 'json', + description: 'Form publish settings', + condition: { field: 'operation', value: ['get_form', 'set_publish_settings'] }, + }, + // Batch Update outputs + replies: { + type: 'json', + description: 'Replies from each update request', + condition: { field: 'operation', value: 'batch_update' }, + }, + writeControl: { + type: 'json', + description: 'Write control with revision IDs', + condition: { field: 'operation', value: 'batch_update' }, + }, + form: { + type: 'json', + description: 'Updated form (if includeFormInResponse is true)', + condition: { field: 'operation', value: 'batch_update' }, + }, + // Watch outputs + watches: { + type: 'json', + description: 'Array of form watches', + condition: { field: 'operation', value: 'list_watches' }, + }, + id: { + type: 'string', + description: 'Watch ID', + condition: { field: 'operation', value: ['create_watch', 'renew_watch'] }, + }, + eventType: { + type: 'string', + description: 'Watch event type', + condition: { field: 'operation', value: ['create_watch', 'renew_watch'] }, + }, + topicName: { + type: 'string', + description: 'Cloud Pub/Sub topic', + condition: { field: 'operation', value: 'create_watch' }, + }, + createTime: { + type: 'string', + description: 'Watch creation time', + condition: { field: 'operation', value: 'create_watch' }, + }, + expireTime: { + type: 'string', + description: 'Watch expiration time', + condition: { field: 'operation', value: ['create_watch', 'renew_watch'] }, + }, + state: { + type: 'string', + description: 'Watch state (ACTIVE, SUSPENDED)', + condition: { field: 'operation', value: ['create_watch', 'renew_watch'] }, + }, + deleted: { + type: 'boolean', + description: 'Whether the watch was deleted', + condition: { field: 'operation', value: 'delete_watch' }, + }, }, triggers: { enabled: true, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index ecd65c14d7..cbfd20ba3a 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -135,7 +135,13 @@ export interface OutputCondition { not?: boolean and?: { field: string - value: string | number | boolean | Array | undefined + value: + | string + | number + | boolean + | Array + | undefined + | null not?: boolean } } diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx index d80841d677..764c1f5ae0 100644 --- a/apps/sim/components/emcn/components/popover/popover.tsx +++ b/apps/sim/components/emcn/components/popover/popover.tsx @@ -170,6 +170,18 @@ interface PopoverContextValue { /** ID of the last hovered item (for hover submenus) */ lastHoveredItem: string | null setLastHoveredItem: (id: string | null) => void + /** Whether keyboard navigation is active. When true, hover styles are suppressed. */ + isKeyboardNav: boolean + setKeyboardNav: (value: boolean) => void + /** Currently selected item index for keyboard navigation */ + selectedIndex: number + setSelectedIndex: (index: number) => void + /** Register a menu item and get its index. Returns a cleanup function. */ + registerItem: (id: string) => number + /** Unregister a menu item */ + unregisterItem: (id: string) => void + /** Get the total number of registered items */ + itemCount: number } const PopoverContext = React.createContext(null) @@ -220,6 +232,23 @@ const Popover: React.FC = ({ const [onFolderSelect, setOnFolderSelect] = React.useState<(() => void) | null>(null) const [searchQuery, setSearchQuery] = React.useState('') const [lastHoveredItem, setLastHoveredItem] = React.useState(null) + const [isKeyboardNav, setIsKeyboardNav] = React.useState(false) + const [selectedIndex, setSelectedIndex] = React.useState(-1) + const registeredItemsRef = React.useRef([]) + + const registerItem = React.useCallback((id: string) => { + if (!registeredItemsRef.current.includes(id)) { + registeredItemsRef.current.push(id) + } + return registeredItemsRef.current.indexOf(id) + }, []) + + const unregisterItem = React.useCallback((id: string) => { + const index = registeredItemsRef.current.indexOf(id) + if (index !== -1) { + registeredItemsRef.current.splice(index, 1) + } + }, []) React.useEffect(() => { if (open === false) { @@ -228,6 +257,9 @@ const Popover: React.FC = ({ setOnFolderSelect(null) setSearchQuery('') setLastHoveredItem(null) + setIsKeyboardNav(false) + setSelectedIndex(-1) + registeredItemsRef.current = [] } }, [open]) @@ -249,6 +281,12 @@ const Popover: React.FC = ({ setOnFolderSelect(null) }, []) + const setKeyboardNav = React.useCallback((value: boolean) => { + setIsKeyboardNav(value) + }, []) + + const itemCount = registeredItemsRef.current.length + const contextValue = React.useMemo( () => ({ openFolder, @@ -264,6 +302,13 @@ const Popover: React.FC = ({ setSearchQuery, lastHoveredItem, setLastHoveredItem, + isKeyboardNav, + setKeyboardNav, + selectedIndex, + setSelectedIndex, + registerItem, + unregisterItem, + itemCount, }), [ openFolder, @@ -276,6 +321,12 @@ const Popover: React.FC = ({ colorScheme, searchQuery, lastHoveredItem, + isKeyboardNav, + setKeyboardNav, + selectedIndex, + registerItem, + unregisterItem, + itemCount, ] ) @@ -382,6 +433,92 @@ const PopoverContent = React.forwardRef< const effectiveSideOffset = sideOffset ?? (side === 'top' ? 20 : 14) + const handleMouseMove = React.useCallback(() => { + if (context?.isKeyboardNav) { + context.setKeyboardNav(false) + } + }, [context]) + + const contentRef = React.useRef(null) + + const mergedRef = React.useCallback( + (node: HTMLDivElement | null) => { + contentRef.current = node + if (typeof ref === 'function') { + ref(node) + } else if (ref) { + ref.current = node + } + }, + [ref] + ) + + React.useEffect(() => { + if (!context) return + + const handleKeyDown = (e: KeyboardEvent) => { + const content = contentRef.current + if (!content) return + + const items = content.querySelectorAll( + '[role="menuitem"]:not([aria-disabled="true"])' + ) + if (items.length === 0) return + + const currentIndex = context.selectedIndex + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + e.stopPropagation() + context.setKeyboardNav(true) + if (currentIndex < 0) { + context.setSelectedIndex(0) + } else { + context.setSelectedIndex(currentIndex < items.length - 1 ? currentIndex + 1 : 0) + } + break + case 'ArrowUp': + e.preventDefault() + e.stopPropagation() + context.setKeyboardNav(true) + if (currentIndex < 0) { + context.setSelectedIndex(items.length - 1) + } else { + context.setSelectedIndex(currentIndex > 0 ? currentIndex - 1 : items.length - 1) + } + break + case 'Enter': + case ' ': + if (currentIndex >= 0) { + e.preventDefault() + e.stopPropagation() + const selectedItem = items[currentIndex] + if (selectedItem) { + selectedItem.click() + } + } + break + } + } + + window.addEventListener('keydown', handleKeyDown, true) + return () => window.removeEventListener('keydown', handleKeyDown, true) + }, [context]) + + React.useEffect(() => { + const content = contentRef.current + if (!content || !context?.isKeyboardNav || context.selectedIndex < 0) return + + const items = content.querySelectorAll( + '[role="menuitem"]:not([aria-disabled="true"])' + ) + const selectedItem = items[context.selectedIndex] + if (selectedItem) { + selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' }) + } + }, [context?.selectedIndex, context?.isKeyboardNav]) + const hasUserWidthConstraint = maxWidth !== undefined || minWidth !== undefined || @@ -425,7 +562,7 @@ const PopoverContent = React.forwardRef< const content = ( ( const variant = context?.variant || 'default' const size = context?.size || 'md' const colorScheme = context?.colorScheme || 'default' + const itemRef = React.useRef(null) + const [itemIndex, setItemIndex] = React.useState(-1) + + const mergedRef = React.useCallback( + (node: HTMLDivElement | null) => { + itemRef.current = node + if (typeof ref === 'function') { + ref(node) + } else if (ref) { + ref.current = node + } + }, + [ref] + ) + + React.useEffect(() => { + if (!itemRef.current) return + const content = itemRef.current.closest('[data-radix-popper-content-wrapper]') + if (!content) return + const items = content.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])') + const index = Array.from(items).indexOf(itemRef.current) + setItemIndex(index) + }, []) if (rootOnly && context?.isInFolder) return null @@ -546,24 +707,36 @@ const PopoverItem = React.forwardRef( } const handleMouseEnter = (e: React.MouseEvent) => { - // Clear last hovered item to close any open hover submenus context?.setLastHoveredItem(null) + if (itemIndex >= 0 && context) { + context.setSelectedIndex(itemIndex) + } onMouseEnter?.(e) } + // Determine if this item is active: + // - Use explicit `active` prop if provided + // - Otherwise use context selectedIndex match + const isActive = + active !== undefined ? active : itemIndex >= 0 && context?.selectedIndex === itemIndex + + // Suppress hover when in keyboard mode to prevent dual highlights + const suppressHover = context?.isKeyboardNav && !isActive + return (
( colorScheme, lastHoveredItem, setLastHoveredItem, + isKeyboardNav, + selectedIndex, + setSelectedIndex, } = usePopoverContext() const [submenuPosition, setSubmenuPosition] = React.useState<{ top: number; left: number }>({ top: 0, left: 0, }) const triggerRef = React.useRef(null) + const [itemIndex, setItemIndex] = React.useState(-1) - // Submenu is open when this folder is the last hovered item (for expandOnHover mode) const isHoverOpen = expandOnHover && lastHoveredItem === id - // Merge refs const mergedRef = React.useCallback( (node: HTMLDivElement | null) => { triggerRef.current = node @@ -689,9 +864,16 @@ const PopoverFolder = React.forwardRef( [ref] ) - // If we're in a folder and this isn't the current one, hide + React.useEffect(() => { + if (!triggerRef.current) return + const content = triggerRef.current.closest('[data-radix-popper-content-wrapper]') + if (!content) return + const items = content.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])') + const index = Array.from(items).indexOf(triggerRef.current) + setItemIndex(index) + }, []) + if (isInFolder && currentFolder !== id) return null - // If this folder is open via click (inline mode), render children directly if (currentFolder === id) return <>{children} const handleClickOpen = () => { @@ -701,22 +883,23 @@ const PopoverFolder = React.forwardRef( const handleClick = (e: React.MouseEvent) => { e.stopPropagation() if (expandOnHover) { - // In hover mode, clicking opens inline and clears hover state setLastHoveredItem(null) } handleClickOpen() } const handleMouseEnter = () => { + if (itemIndex >= 0) { + setSelectedIndex(itemIndex) + } + if (!expandOnHover) return - // Calculate position for submenu if (triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect() const parentPopover = triggerRef.current.closest('[data-radix-popper-content-wrapper]') const parentRect = parentPopover?.getBoundingClientRect() - // Position to the right of the parent popover with a small gap setSubmenuPosition({ top: rect.top, left: parentRect ? parentRect.right + 4 : rect.right + 4, @@ -727,6 +910,11 @@ const PopoverFolder = React.forwardRef( onOpen?.() } + const isActive = active !== undefined ? active : itemIndex >= 0 && selectedIndex === itemIndex + + // Suppress hover when in keyboard mode to prevent dual highlights + const suppressHover = isKeyboardNav && !isActive && !isHoverOpen + return ( <>
( STYLES.itemBase, STYLES.colorScheme[colorScheme].text, STYLES.size[size].item, - getItemStateClasses(variant, colorScheme, !!active || isHoverOpen), + getItemStateClasses(variant, colorScheme, isActive || isHoverOpen), + suppressHover && 'hover:!bg-transparent', className )} role='menuitem' aria-haspopup='true' aria-expanded={isHoverOpen} + aria-selected={isActive} onClick={handleClick} onMouseEnter={handleMouseEnter} {...props} diff --git a/apps/sim/hooks/queries/general-settings.ts b/apps/sim/hooks/queries/general-settings.ts index 1fc8a5fd01..f85df4a440 100644 --- a/apps/sim/hooks/queries/general-settings.ts +++ b/apps/sim/hooks/queries/general-settings.ts @@ -1,7 +1,6 @@ import { createLogger } from '@sim/logger' import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { syncThemeToNextThemes } from '@/lib/core/utils/theme' -import { useGeneralStore } from '@/stores/settings/general' const logger = createLogger('GeneralSettingsQuery') @@ -53,48 +52,16 @@ async function fetchGeneralSettings(): Promise { } } -/** - * Sync React Query cache to Zustand store and next-themes. - * This ensures the rest of the app (which uses Zustand) stays in sync. - * Uses shallow comparison to prevent unnecessary updates and flickering. - * @param settings - The general settings to sync - */ -function syncSettingsToZustand(settings: GeneralSettings) { - const store = useGeneralStore.getState() - - const newSettings = { - isAutoConnectEnabled: settings.autoConnect, - showTrainingControls: settings.showTrainingControls, - superUserModeEnabled: settings.superUserModeEnabled, - theme: settings.theme, - telemetryEnabled: settings.telemetryEnabled, - isBillingUsageNotificationsEnabled: settings.billingUsageNotificationsEnabled, - isErrorNotificationsEnabled: settings.errorNotificationsEnabled, - snapToGridSize: settings.snapToGridSize, - showActionBar: settings.showActionBar, - } - - const hasChanges = Object.entries(newSettings).some( - ([key, value]) => store[key as keyof typeof newSettings] !== value - ) - - if (hasChanges) { - store.setSettings(newSettings) - } - - syncThemeToNextThemes(settings.theme) -} - /** * Hook to fetch general settings. - * Syncs to Zustand store only on successful fetch (not on cache updates from mutations). + * TanStack Query is now the single source of truth for general settings. */ export function useGeneralSettings() { return useQuery({ queryKey: generalSettingsKeys.settings(), queryFn: async () => { const settings = await fetchGeneralSettings() - syncSettingsToZustand(settings) + syncThemeToNextThemes(settings.theme) return settings }, staleTime: 60 * 60 * 1000, @@ -102,6 +69,41 @@ export function useGeneralSettings() { }) } +/** + * Convenience selector hooks for individual settings. + * These provide a simple API for components that only need a single setting value. + */ + +export function useAutoConnect(): boolean { + const { data } = useGeneralSettings() + return data?.autoConnect ?? true +} + +export function useShowTrainingControls(): boolean { + const { data } = useGeneralSettings() + return data?.showTrainingControls ?? false +} + +export function useSnapToGridSize(): number { + const { data } = useGeneralSettings() + return data?.snapToGridSize ?? 0 +} + +export function useShowActionBar(): boolean { + const { data } = useGeneralSettings() + return data?.showActionBar ?? true +} + +export function useBillingUsageNotifications(): boolean { + const { data } = useGeneralSettings() + return data?.billingUsageNotificationsEnabled ?? true +} + +export function useErrorNotificationsEnabled(): boolean { + const { data } = useGeneralSettings() + return data?.errorNotificationsEnabled ?? true +} + /** * Update general settings mutation */ @@ -141,7 +143,10 @@ export function useUpdateGeneralSetting() { } queryClient.setQueryData(generalSettingsKeys.settings(), newSettings) - syncSettingsToZustand(newSettings) + + if (key === 'theme') { + syncThemeToNextThemes(value as GeneralSettings['theme']) + } } return { previousSettings } @@ -149,7 +154,7 @@ export function useUpdateGeneralSetting() { onError: (err, _variables, context) => { if (context?.previousSettings) { queryClient.setQueryData(generalSettingsKeys.settings(), context.previousSettings) - syncSettingsToZustand(context.previousSettings) + syncThemeToNextThemes(context.previousSettings.theme) } logger.error('Failed to update setting:', err) }, diff --git a/apps/sim/hooks/selectors/resolution.ts b/apps/sim/hooks/selectors/resolution.ts index f018953e73..54837537cf 100644 --- a/apps/sim/hooks/selectors/resolution.ts +++ b/apps/sim/hooks/selectors/resolution.ts @@ -110,6 +110,8 @@ function resolveFileSelector( return { key: 'google.drive', context, allowSearch: true } case 'google-slides': return { key: 'google.drive', context, allowSearch: true } + case 'google-forms': + return { key: 'google.drive', context, allowSearch: true } case 'onedrive': { const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders' return { key, context, allowSearch: true } diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 8a3b8a3cbc..0ff169a2fd 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -884,6 +884,8 @@ export const auth = betterAuth({ scopes: [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/forms.body', 'https://www.googleapis.com/auth/forms.responses.readonly', ], prompt: 'consent', diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index d00fea2e4e..2f812b13f4 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -96,13 +96,15 @@ export const OAUTH_PROVIDERS: Record = { }, 'google-forms': { name: 'Google Forms', - description: 'Retrieve Google Form responses.', + description: 'Create, modify, and read Google Forms.', providerId: 'google-forms', icon: GoogleFormsIcon, baseProviderIcon: GoogleIcon, scopes: [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/forms.body', 'https://www.googleapis.com/auth/forms.responses.readonly', ], }, diff --git a/apps/sim/lib/workflows/blocks/block-outputs.ts b/apps/sim/lib/workflows/blocks/block-outputs.ts index c953a11968..c914227ab5 100644 --- a/apps/sim/lib/workflows/blocks/block-outputs.ts +++ b/apps/sim/lib/workflows/blocks/block-outputs.ts @@ -62,8 +62,11 @@ function evaluateOutputCondition( let andMatches: boolean if (Array.isArray(condition.and.value)) { - andMatches = + const primitiveMatch = isConditionPrimitive(andFieldValue) && condition.and.value.includes(andFieldValue) + const undefinedMatch = andFieldValue === undefined && condition.and.value.includes(undefined) + const nullMatch = andFieldValue === null && condition.and.value.includes(null) + andMatches = primitiveMatch || undefinedMatch || nullMatch } else { andMatches = andFieldValue === condition.and.value } @@ -95,7 +98,9 @@ function filterOutputsByCondition( } const condition = value.condition as OutputCondition | undefined - if (!condition || evaluateOutputCondition(condition, subBlocks)) { + const passes = !condition || evaluateOutputCondition(condition, subBlocks) + + if (passes) { const { condition: _, ...rest } = value filtered[key] = rest } @@ -565,11 +570,32 @@ export function getToolOutputs(blockConfig: BlockConfig, operation: string): Rec * * @param blockConfig - The block configuration containing tools config * @param operation - The selected operation for the tool + * @param subBlocks - Optional subBlock values for condition evaluation * @returns Array of output paths for the tool, or empty array on error */ -export function getToolOutputPaths(blockConfig: BlockConfig, operation: string): string[] { +export function getToolOutputPaths( + blockConfig: BlockConfig, + operation: string, + subBlocks?: Record +): string[] { const outputs = getToolOutputs(blockConfig, operation) + if (!outputs || Object.keys(outputs).length === 0) return [] + + if (subBlocks && blockConfig.outputs) { + const filteredBlockOutputs = filterOutputsByCondition(blockConfig.outputs, subBlocks) + const allowedKeys = new Set(Object.keys(filteredBlockOutputs)) + + const filteredOutputs: Record = {} + for (const [key, value] of Object.entries(outputs)) { + if (allowedKeys.has(key)) { + filteredOutputs[key] = value + } + } + + return generateOutputPaths(filteredOutputs) + } + return generateOutputPaths(outputs) } diff --git a/apps/sim/stores/settings/general/index.ts b/apps/sim/stores/settings/general/index.ts deleted file mode 100644 index 8c35af4736..0000000000 --- a/apps/sim/stores/settings/general/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useGeneralStore } from './store' -export type { General, GeneralStore, UserSettings } from './types' diff --git a/apps/sim/stores/settings/general/store.ts b/apps/sim/stores/settings/general/store.ts deleted file mode 100644 index f1bc3362b4..0000000000 --- a/apps/sim/stores/settings/general/store.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createLogger } from '@sim/logger' -import { create } from 'zustand' -import { devtools } from 'zustand/middleware' -import type { General, GeneralStore } from './types' - -const logger = createLogger('GeneralStore') - -const initialState: General = { - isAutoConnectEnabled: true, - showTrainingControls: false, - superUserModeEnabled: true, - theme: 'dark', - telemetryEnabled: true, - isBillingUsageNotificationsEnabled: true, - isErrorNotificationsEnabled: true, - snapToGridSize: 0, - showActionBar: true, -} - -export const useGeneralStore = create()( - devtools( - (set) => ({ - ...initialState, - setSettings: (settings) => { - logger.debug('Updating general settings store', { - keys: Object.keys(settings), - }) - set((state) => ({ - ...state, - ...settings, - })) - }, - reset: () => set(initialState), - }), - { name: 'general-store' } - ) -) diff --git a/apps/sim/stores/settings/general/types.ts b/apps/sim/stores/settings/general/types.ts deleted file mode 100644 index d4b11e4ae6..0000000000 --- a/apps/sim/stores/settings/general/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface General { - isAutoConnectEnabled: boolean - showTrainingControls: boolean - superUserModeEnabled: boolean - theme: 'system' | 'light' | 'dark' - telemetryEnabled: boolean - isBillingUsageNotificationsEnabled: boolean - isErrorNotificationsEnabled: boolean - snapToGridSize: number - showActionBar: boolean -} - -export interface GeneralStore extends General { - setSettings: (settings: Partial) => void - reset: () => void -} - -export type UserSettings = { - theme: 'system' | 'light' | 'dark' - autoConnect: boolean - showTrainingControls: boolean - superUserModeEnabled: boolean - telemetryEnabled: boolean - isBillingUsageNotificationsEnabled: boolean - errorNotificationsEnabled: boolean - snapToGridSize: number - showActionBar: boolean -} diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index d6becc4929..a2921881a8 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -2,10 +2,11 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' import { createJSONStorage, devtools, persist } from 'zustand/middleware' import { redactApiKeys } from '@/lib/core/security/redaction' +import { getQueryClient } from '@/app/_shell/providers/query-provider' import type { NormalizedBlockOutput } from '@/executor/types' +import { type GeneralSettings, generalSettingsKeys } from '@/hooks/queries/general-settings' import { useExecutionStore } from '@/stores/execution' import { useNotificationStore } from '@/stores/notifications' -import { useGeneralStore } from '@/stores/settings/general' import { indexedDBStorage } from '@/stores/terminal/console/storage' import type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from '@/stores/terminal/console/types' @@ -153,7 +154,10 @@ export const useTerminalConsoleStore = create()( const newEntry = get().entries[0] if (newEntry?.error) { - const { isErrorNotificationsEnabled } = useGeneralStore.getState() + const settings = getQueryClient().getQueryData( + generalSettingsKeys.settings() + ) + const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true if (isErrorNotificationsEnabled) { try { diff --git a/apps/sim/tools/github/compare_commits.ts b/apps/sim/tools/github/compare_commits.ts index 02ee8a5689..aa90903e31 100644 --- a/apps/sim/tools/github/compare_commits.ts +++ b/apps/sim/tools/github/compare_commits.ts @@ -241,16 +241,94 @@ export const compareCommitsV2Tool: ToolConfig = { }, outputs: { - status: { type: 'string', description: 'Comparison status' }, - ahead_by: { type: 'number', description: 'Commits ahead' }, - behind_by: { type: 'number', description: 'Commits behind' }, - total_commits: { type: 'number', description: 'Total commits' }, - html_url: { type: 'string', description: 'Web URL' }, - diff_url: { type: 'string', description: 'Diff URL' }, - patch_url: { type: 'string', description: 'Patch URL' }, - base_commit: { type: 'object', description: 'Base commit' }, - merge_base_commit: { type: 'object', description: 'Merge base' }, - commits: { type: 'array', description: 'Commits between' }, - files: { type: 'array', description: 'Changed files' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + permalink_url: { type: 'string', description: 'Permanent link URL' }, + diff_url: { type: 'string', description: 'Diff download URL' }, + patch_url: { type: 'string', description: 'Patch download URL' }, + status: { + type: 'string', + description: 'Comparison status (ahead, behind, identical, diverged)', + }, + ahead_by: { type: 'number', description: 'Commits head is ahead of base' }, + behind_by: { type: 'number', description: 'Commits head is behind base' }, + total_commits: { type: 'number', description: 'Total commits in comparison' }, + base_commit: { + type: 'object', + description: 'Base commit object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + commit: { + type: 'object', + description: 'Commit data', + properties: { + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Git author (name, email, date)' }, + committer: { type: 'object', description: 'Git committer (name, email, date)' }, + }, + }, + author: { type: 'object', description: 'GitHub user (author)', optional: true }, + committer: { type: 'object', description: 'GitHub user (committer)', optional: true }, + }, + }, + merge_base_commit: { + type: 'object', + description: 'Merge base commit object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + }, + }, + commits: { + type: 'array', + description: 'Commits between base and head', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + html_url: { type: 'string', description: 'Web URL' }, + commit: { + type: 'object', + description: 'Commit data', + properties: { + message: { type: 'string', description: 'Commit message' }, + author: { type: 'object', description: 'Git author (name, email, date)' }, + committer: { type: 'object', description: 'Git committer (name, email, date)' }, + }, + }, + author: { type: 'object', description: 'GitHub user', optional: true }, + committer: { type: 'object', description: 'GitHub user', optional: true }, + }, + }, + }, + files: { + type: 'array', + description: 'Changed files (diff entries)', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Blob SHA', optional: true }, + filename: { type: 'string', description: 'File path' }, + status: { + type: 'string', + description: + 'Change status (added, removed, modified, renamed, copied, changed, unchanged)', + }, + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + changes: { type: 'number', description: 'Total changes' }, + blob_url: { type: 'string', description: 'Blob URL' }, + raw_url: { type: 'string', description: 'Raw file URL' }, + contents_url: { type: 'string', description: 'Contents API URL' }, + patch: { type: 'string', description: 'Diff patch', optional: true }, + previous_filename: { + type: 'string', + description: 'Previous filename (for renames)', + optional: true, + }, + }, + }, + }, }, } diff --git a/apps/sim/tools/github/create_comment_reaction.ts b/apps/sim/tools/github/create_comment_reaction.ts index e27d897a7f..718a43de64 100644 --- a/apps/sim/tools/github/create_comment_reaction.ts +++ b/apps/sim/tools/github/create_comment_reaction.ts @@ -131,8 +131,26 @@ export const createCommentReactionV2Tool: ToolConfig = { outputs: { id: { type: 'string', description: 'Gist ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + url: { type: 'string', description: 'API URL' }, html_url: { type: 'string', description: 'Web URL' }, + forks_url: { type: 'string', description: 'Forks API URL' }, + commits_url: { type: 'string', description: 'Commits API URL' }, git_pull_url: { type: 'string', description: 'Git pull URL' }, git_push_url: { type: 'string', description: 'Git push URL' }, - description: { type: 'string', description: 'Description', optional: true }, - public: { type: 'boolean', description: 'Is public' }, - created_at: { type: 'string', description: 'Creation date' }, - updated_at: { type: 'string', description: 'Update date' }, - files: { type: 'object', description: 'Files in gist' }, - owner: { type: 'object', description: 'Owner info' }, + description: { type: 'string', description: 'Gist description', optional: true }, + public: { type: 'boolean', description: 'Whether gist is public' }, + truncated: { type: 'boolean', description: 'Whether files are truncated' }, + comments: { type: 'number', description: 'Number of comments' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + files: { + type: 'object', + description: + 'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content)', + }, + owner: { + type: 'object', + description: 'Gist owner', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, } diff --git a/apps/sim/tools/github/create_issue_reaction.ts b/apps/sim/tools/github/create_issue_reaction.ts index dfb30fd5b8..7fc5d4d68d 100644 --- a/apps/sim/tools/github/create_issue_reaction.ts +++ b/apps/sim/tools/github/create_issue_reaction.ts @@ -131,8 +131,26 @@ export const createIssueReactionV2Tool: ToolConfig = { outputs: { id: { type: 'number', description: 'Repository ID' }, - full_name: { type: 'string', description: 'Full name' }, - html_url: { type: 'string', description: 'Web URL' }, - clone_url: { type: 'string', description: 'Clone URL' }, - ssh_url: { type: 'string', description: 'SSH URL' }, - default_branch: { type: 'string', description: 'Default branch' }, - fork: { type: 'boolean', description: 'Is a fork' }, - parent: { type: 'object', description: 'Parent repository', optional: true }, - owner: { type: 'object', description: 'Owner' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + name: { type: 'string', description: 'Repository name' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + private: { type: 'boolean', description: 'Whether repository is private' }, + description: { type: 'string', description: 'Repository description', optional: true }, + html_url: { type: 'string', description: 'GitHub web URL' }, + url: { type: 'string', description: 'API URL' }, + clone_url: { type: 'string', description: 'HTTPS clone URL' }, + ssh_url: { type: 'string', description: 'SSH clone URL' }, + git_url: { type: 'string', description: 'Git protocol URL' }, + default_branch: { type: 'string', description: 'Default branch name' }, + fork: { type: 'boolean', description: 'Whether this is a fork' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + pushed_at: { type: 'string', description: 'Last push timestamp', optional: true }, + owner: { + type: 'object', + description: 'Fork owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + parent: { + type: 'object', + description: 'Parent repository (source of the fork)', + optional: true, + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + html_url: { type: 'string', description: 'Web URL' }, + description: { type: 'string', description: 'Description', optional: true }, + owner: { + type: 'object', + description: 'Parent owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + }, + }, + }, + }, + source: { + type: 'object', + description: 'Source repository (ultimate origin)', + optional: true, + properties: { + id: { type: 'number', description: 'Repository ID' }, + full_name: { type: 'string', description: 'Full name' }, + html_url: { type: 'string', description: 'Web URL' }, + }, + }, }, } diff --git a/apps/sim/tools/github/get_commit.ts b/apps/sim/tools/github/get_commit.ts index b71ee941e1..1ea0431806 100644 --- a/apps/sim/tools/github/get_commit.ts +++ b/apps/sim/tools/github/get_commit.ts @@ -191,12 +191,128 @@ export const getCommitV2Tool: ToolConfig = { outputs: { sha: { type: 'string', description: 'Commit SHA' }, - html_url: { type: 'string', description: 'Web URL' }, - commit: { type: 'object', description: 'Commit data' }, - author: { type: 'object', description: 'GitHub user', optional: true }, - committer: { type: 'object', description: 'GitHub user', optional: true }, - stats: { type: 'object', description: 'Change stats', optional: true }, - files: { type: 'array', description: 'Changed files' }, - parents: { type: 'array', description: 'Parent commits' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + url: { type: 'string', description: 'API URL' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + commit: { + type: 'object', + description: 'Core commit data', + properties: { + url: { type: 'string', description: 'Commit API URL' }, + message: { type: 'string', description: 'Commit message' }, + comment_count: { type: 'number', description: 'Number of comments' }, + author: { + type: 'object', + description: 'Git author', + properties: { + name: { type: 'string', description: 'Author name' }, + email: { type: 'string', description: 'Author email' }, + date: { type: 'string', description: 'Author date (ISO 8601)' }, + }, + }, + committer: { + type: 'object', + description: 'Git committer', + properties: { + name: { type: 'string', description: 'Committer name' }, + email: { type: 'string', description: 'Committer email' }, + date: { type: 'string', description: 'Commit date (ISO 8601)' }, + }, + }, + tree: { + type: 'object', + description: 'Tree object', + properties: { + sha: { type: 'string', description: 'Tree SHA' }, + url: { type: 'string', description: 'Tree API URL' }, + }, + }, + verification: { + type: 'object', + description: 'Signature verification', + properties: { + verified: { type: 'boolean', description: 'Whether signature is verified' }, + reason: { type: 'string', description: 'Verification reason' }, + signature: { type: 'string', description: 'GPG signature', optional: true }, + payload: { type: 'string', description: 'Signed payload', optional: true }, + }, + }, + }, + }, + author: { + type: 'object', + description: 'GitHub user (author)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + }, + }, + committer: { + type: 'object', + description: 'GitHub user (committer)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + }, + }, + stats: { + type: 'object', + description: 'Change statistics', + optional: true, + properties: { + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + total: { type: 'number', description: 'Total changes' }, + }, + }, + files: { + type: 'array', + description: 'Changed files (diff entries)', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Blob SHA', optional: true }, + filename: { type: 'string', description: 'File path' }, + status: { + type: 'string', + description: + 'Change status (added, removed, modified, renamed, copied, changed, unchanged)', + }, + additions: { type: 'number', description: 'Lines added' }, + deletions: { type: 'number', description: 'Lines deleted' }, + changes: { type: 'number', description: 'Total changes' }, + blob_url: { type: 'string', description: 'Blob URL' }, + raw_url: { type: 'string', description: 'Raw file URL' }, + contents_url: { type: 'string', description: 'Contents API URL' }, + patch: { type: 'string', description: 'Diff patch', optional: true }, + previous_filename: { + type: 'string', + description: 'Previous filename (for renames)', + optional: true, + }, + }, + }, + }, + parents: { + type: 'array', + description: 'Parent commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Parent SHA' }, + url: { type: 'string', description: 'Parent API URL' }, + html_url: { type: 'string', description: 'Parent web URL' }, + }, + }, + }, }, } diff --git a/apps/sim/tools/github/get_gist.ts b/apps/sim/tools/github/get_gist.ts index 67dee27ba3..8f7d10b421 100644 --- a/apps/sim/tools/github/get_gist.ts +++ b/apps/sim/tools/github/get_gist.ts @@ -165,13 +165,52 @@ export const getGistV2Tool: ToolConfig = { outputs: { id: { type: 'string', description: 'Gist ID' }, - html_url: { type: 'string', description: 'Web URL' }, - description: { type: 'string', description: 'Description', optional: true }, - public: { type: 'boolean', description: 'Is public' }, - created_at: { type: 'string', description: 'Creation date' }, - updated_at: { type: 'string', description: 'Update date' }, - files: { type: 'object', description: 'Files with content' }, - owner: { type: 'object', description: 'Owner info' }, - comments: { type: 'number', description: 'Comment count' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + url: { type: 'string', description: 'API URL' }, + forks_url: { type: 'string', description: 'Forks API URL' }, + commits_url: { type: 'string', description: 'Commits API URL' }, + git_pull_url: { type: 'string', description: 'Git clone URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Gist description', optional: true }, + public: { type: 'boolean', description: 'Whether gist is public' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + comments: { type: 'number', description: 'Number of comments' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + truncated: { type: 'boolean', description: 'Whether content is truncated' }, + files: { + type: 'object', + description: 'Files in the gist (keyed by filename)', + properties: { + '[filename]': { + type: 'object', + description: 'File object', + properties: { + filename: { type: 'string', description: 'File name' }, + type: { type: 'string', description: 'MIME type' }, + language: { type: 'string', description: 'Programming language', optional: true }, + raw_url: { type: 'string', description: 'Raw file URL' }, + size: { type: 'number', description: 'File size in bytes' }, + truncated: { type: 'boolean', description: 'Whether content is truncated' }, + content: { type: 'string', description: 'File content' }, + }, + }, + }, + }, + owner: { + type: 'object', + description: 'Gist owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, } diff --git a/apps/sim/tools/github/get_milestone.ts b/apps/sim/tools/github/get_milestone.ts index e8b24ce4ea..1beb4ede5e 100644 --- a/apps/sim/tools/github/get_milestone.ts +++ b/apps/sim/tools/github/get_milestone.ts @@ -153,15 +153,35 @@ export const getMilestoneV2Tool: ToolConfig = { }, outputs: { + id: { type: 'number', description: 'Milestone ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, number: { type: 'number', description: 'Milestone number' }, - title: { type: 'string', description: 'Title' }, - description: { type: 'string', description: 'Description', optional: true }, - state: { type: 'string', description: 'State' }, - html_url: { type: 'string', description: 'Web URL' }, - due_on: { type: 'string', description: 'Due date', optional: true }, - open_issues: { type: 'number', description: 'Open issues' }, - closed_issues: { type: 'number', description: 'Closed issues' }, - closed_at: { type: 'string', description: 'Close date', optional: true }, - creator: { type: 'object', description: 'Creator' }, + title: { type: 'string', description: 'Milestone title' }, + description: { type: 'string', description: 'Milestone description', optional: true }, + state: { type: 'string', description: 'State (open or closed)' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + labels_url: { type: 'string', description: 'Labels API URL' }, + due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true }, + open_issues: { type: 'number', description: 'Number of open issues' }, + closed_issues: { type: 'number', description: 'Number of closed issues' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + closed_at: { type: 'string', description: 'Close timestamp', optional: true }, + creator: { + type: 'object', + description: 'Milestone creator', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, } diff --git a/apps/sim/tools/github/list_commits.ts b/apps/sim/tools/github/list_commits.ts index 4781bfacb7..443ac7d3b9 100644 --- a/apps/sim/tools/github/list_commits.ts +++ b/apps/sim/tools/github/list_commits.ts @@ -230,11 +230,97 @@ export const listCommitsV2Tool: ToolConfig = { type: 'object', properties: { sha: { type: 'string', description: 'Commit SHA' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, html_url: { type: 'string', description: 'Web URL' }, - commit: { type: 'object', description: 'Commit data' }, - author: { type: 'object', description: 'GitHub user', optional: true }, - committer: { type: 'object', description: 'GitHub user', optional: true }, - parents: { type: 'array', description: 'Parent commits' }, + url: { type: 'string', description: 'API URL' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + commit: { + type: 'object', + description: 'Core commit data', + properties: { + url: { type: 'string', description: 'Commit API URL' }, + message: { type: 'string', description: 'Commit message' }, + comment_count: { type: 'number', description: 'Number of comments' }, + author: { + type: 'object', + description: 'Git author', + properties: { + name: { type: 'string', description: 'Author name' }, + email: { type: 'string', description: 'Author email' }, + date: { type: 'string', description: 'Author date (ISO 8601)' }, + }, + }, + committer: { + type: 'object', + description: 'Git committer', + properties: { + name: { type: 'string', description: 'Committer name' }, + email: { type: 'string', description: 'Committer email' }, + date: { type: 'string', description: 'Commit date (ISO 8601)' }, + }, + }, + tree: { + type: 'object', + description: 'Tree object', + properties: { + sha: { type: 'string', description: 'Tree SHA' }, + url: { type: 'string', description: 'Tree API URL' }, + }, + }, + verification: { + type: 'object', + description: 'Signature verification', + properties: { + verified: { type: 'boolean', description: 'Whether signature is verified' }, + reason: { type: 'string', description: 'Verification reason' }, + signature: { type: 'string', description: 'GPG signature', optional: true }, + payload: { type: 'string', description: 'Signed payload', optional: true }, + }, + }, + }, + }, + author: { + type: 'object', + description: 'GitHub user (author)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + committer: { + type: 'object', + description: 'GitHub user (committer)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + parents: { + type: 'array', + description: 'Parent commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Parent SHA' }, + url: { type: 'string', description: 'Parent API URL' }, + html_url: { type: 'string', description: 'Parent web URL' }, + }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/list_forks.ts b/apps/sim/tools/github/list_forks.ts index c967194d9c..c7bcddbcb0 100644 --- a/apps/sim/tools/github/list_forks.ts +++ b/apps/sim/tools/github/list_forks.ts @@ -188,11 +188,41 @@ export const listForksV2Tool: ToolConfig = { type: 'object', properties: { id: { type: 'number', description: 'Repository ID' }, - full_name: { type: 'string', description: 'Full name' }, - html_url: { type: 'string', description: 'Web URL' }, - owner: { type: 'object', description: 'Owner' }, - stargazers_count: { type: 'number', description: 'Stars' }, - forks_count: { type: 'number', description: 'Forks' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + name: { type: 'string', description: 'Repository name' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + private: { type: 'boolean', description: 'Whether repository is private' }, + description: { type: 'string', description: 'Repository description', optional: true }, + html_url: { type: 'string', description: 'GitHub web URL' }, + url: { type: 'string', description: 'API URL' }, + fork: { type: 'boolean', description: 'Whether this is a fork' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + pushed_at: { type: 'string', description: 'Last push timestamp', optional: true }, + size: { type: 'number', description: 'Repository size in KB' }, + stargazers_count: { type: 'number', description: 'Number of stars' }, + watchers_count: { type: 'number', description: 'Number of watchers' }, + forks_count: { type: 'number', description: 'Number of forks' }, + open_issues_count: { type: 'number', description: 'Number of open issues' }, + language: { type: 'string', description: 'Primary programming language', optional: true }, + default_branch: { type: 'string', description: 'Default branch name' }, + visibility: { type: 'string', description: 'Repository visibility' }, + archived: { type: 'boolean', description: 'Whether repository is archived' }, + disabled: { type: 'boolean', description: 'Whether repository is disabled' }, + owner: { + type: 'object', + description: 'Fork owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/list_gists.ts b/apps/sim/tools/github/list_gists.ts index 40d54ebac5..50d16db40b 100644 --- a/apps/sim/tools/github/list_gists.ts +++ b/apps/sim/tools/github/list_gists.ts @@ -188,11 +188,40 @@ export const listGistsV2Tool: ToolConfig = { type: 'object', properties: { id: { type: 'string', description: 'Gist ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + url: { type: 'string', description: 'API URL' }, html_url: { type: 'string', description: 'Web URL' }, - description: { type: 'string', description: 'Description', optional: true }, - public: { type: 'boolean', description: 'Is public' }, - files: { type: 'object', description: 'Files' }, - owner: { type: 'object', description: 'Owner' }, + forks_url: { type: 'string', description: 'Forks API URL' }, + commits_url: { type: 'string', description: 'Commits API URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Gist description', optional: true }, + public: { type: 'boolean', description: 'Whether gist is public' }, + truncated: { type: 'boolean', description: 'Whether files are truncated' }, + comments: { type: 'number', description: 'Number of comments' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + files: { + type: 'object', + description: + 'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size)', + }, + owner: { + type: 'object', + description: 'Gist owner', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/list_milestones.ts b/apps/sim/tools/github/list_milestones.ts index 052deabfba..2e49c663b1 100644 --- a/apps/sim/tools/github/list_milestones.ts +++ b/apps/sim/tools/github/list_milestones.ts @@ -212,12 +212,36 @@ export const listMilestonesV2Tool: ToolConfig = { items: { type: 'object', properties: { + id: { type: 'number', description: 'Milestone ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, number: { type: 'number', description: 'Milestone number' }, - title: { type: 'string', description: 'Title' }, - state: { type: 'string', description: 'State' }, - html_url: { type: 'string', description: 'Web URL' }, - open_issues: { type: 'number', description: 'Open issues' }, - closed_issues: { type: 'number', description: 'Closed issues' }, + title: { type: 'string', description: 'Milestone title' }, + description: { type: 'string', description: 'Milestone description', optional: true }, + state: { type: 'string', description: 'State (open or closed)' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + labels_url: { type: 'string', description: 'Labels API URL' }, + due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true }, + open_issues: { type: 'number', description: 'Number of open issues' }, + closed_issues: { type: 'number', description: 'Number of closed issues' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + closed_at: { type: 'string', description: 'Close timestamp', optional: true }, + creator: { + type: 'object', + description: 'Milestone creator', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/list_stargazers.ts b/apps/sim/tools/github/list_stargazers.ts index bd0e7306ca..dc2ff5f1d5 100644 --- a/apps/sim/tools/github/list_stargazers.ts +++ b/apps/sim/tools/github/list_stargazers.ts @@ -161,9 +161,18 @@ export const listStargazersV2Tool: ToolConfig = { properties: { login: { type: 'string', description: 'Username' }, id: { type: 'number', description: 'User ID' }, - avatar_url: { type: 'string', description: 'Avatar URL' }, - html_url: { type: 'string', description: 'Profile URL' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + gravatar_id: { type: 'string', description: 'Gravatar ID' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + followers_url: { type: 'string', description: 'Followers API URL' }, + following_url: { type: 'string', description: 'Following API URL' }, + gists_url: { type: 'string', description: 'Gists API URL' }, + starred_url: { type: 'string', description: 'Starred API URL' }, + repos_url: { type: 'string', description: 'Repos API URL' }, type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, }, }, }, diff --git a/apps/sim/tools/github/search_code.ts b/apps/sim/tools/github/search_code.ts index 8ad41ccbc2..f74594067b 100644 --- a/apps/sim/tools/github/search_code.ts +++ b/apps/sim/tools/github/search_code.ts @@ -202,8 +202,67 @@ export const searchCodeV2Tool: ToolConfig = { name: { type: 'string', description: 'File name' }, path: { type: 'string', description: 'File path' }, sha: { type: 'string', description: 'Blob SHA' }, + url: { type: 'string', description: 'API URL' }, + git_url: { type: 'string', description: 'Git blob URL' }, html_url: { type: 'string', description: 'GitHub web URL' }, - repository: { type: 'object', description: 'Repository object' }, + score: { type: 'number', description: 'Search relevance score' }, + repository: { + type: 'object', + description: 'Repository containing the code', + properties: { + id: { type: 'number', description: 'Repository ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + name: { type: 'string', description: 'Repository name' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + private: { type: 'boolean', description: 'Whether repository is private' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + description: { + type: 'string', + description: 'Repository description', + optional: true, + }, + fork: { type: 'boolean', description: 'Whether this is a fork' }, + url: { type: 'string', description: 'API URL' }, + owner: { + type: 'object', + description: 'Repository owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + }, + }, + text_matches: { + type: 'array', + description: 'Text matches showing context', + items: { + type: 'object', + properties: { + object_url: { type: 'string', description: 'Object URL' }, + object_type: { type: 'string', description: 'Object type', optional: true }, + property: { type: 'string', description: 'Property matched' }, + fragment: { type: 'string', description: 'Text fragment with match' }, + matches: { + type: 'array', + description: 'Match indices', + items: { + type: 'object', + properties: { + text: { type: 'string', description: 'Matched text' }, + indices: { type: 'array', description: 'Start and end indices' }, + }, + }, + }, + }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/search_commits.ts b/apps/sim/tools/github/search_commits.ts index aafbe47f8d..ea29484019 100644 --- a/apps/sim/tools/github/search_commits.ts +++ b/apps/sim/tools/github/search_commits.ts @@ -212,11 +212,119 @@ export const searchCommitsV2Tool: ToolConfig = { type: 'object', properties: { sha: { type: 'string', description: 'Commit SHA' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, html_url: { type: 'string', description: 'Web URL' }, - commit: { type: 'object', description: 'Commit data' }, - author: { type: 'object', description: 'GitHub user', optional: true }, - committer: { type: 'object', description: 'GitHub user', optional: true }, - repository: { type: 'object', description: 'Repository' }, + url: { type: 'string', description: 'API URL' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + score: { type: 'number', description: 'Search relevance score' }, + commit: { + type: 'object', + description: 'Core commit data', + properties: { + url: { type: 'string', description: 'Commit API URL' }, + message: { type: 'string', description: 'Commit message' }, + comment_count: { type: 'number', description: 'Number of comments' }, + author: { + type: 'object', + description: 'Git author', + properties: { + name: { type: 'string', description: 'Author name' }, + email: { type: 'string', description: 'Author email' }, + date: { type: 'string', description: 'Author date (ISO 8601)' }, + }, + }, + committer: { + type: 'object', + description: 'Git committer', + properties: { + name: { type: 'string', description: 'Committer name' }, + email: { type: 'string', description: 'Committer email' }, + date: { type: 'string', description: 'Commit date (ISO 8601)' }, + }, + }, + tree: { + type: 'object', + description: 'Tree object', + properties: { + sha: { type: 'string', description: 'Tree SHA' }, + url: { type: 'string', description: 'Tree API URL' }, + }, + }, + }, + }, + author: { + type: 'object', + description: 'GitHub user (author)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + committer: { + type: 'object', + description: 'GitHub user (committer)', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + repository: { + type: 'object', + description: 'Repository containing the commit', + properties: { + id: { type: 'number', description: 'Repository ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + name: { type: 'string', description: 'Repository name' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + private: { type: 'boolean', description: 'Whether repository is private' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + description: { + type: 'string', + description: 'Repository description', + optional: true, + }, + owner: { + type: 'object', + description: 'Repository owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + }, + }, + parents: { + type: 'array', + description: 'Parent commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Parent SHA' }, + url: { type: 'string', description: 'Parent API URL' }, + html_url: { type: 'string', description: 'Parent web URL' }, + }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/search_issues.ts b/apps/sim/tools/github/search_issues.ts index 9cab3e58bb..19f716f035 100644 --- a/apps/sim/tools/github/search_issues.ts +++ b/apps/sim/tools/github/search_issues.ts @@ -222,17 +222,110 @@ export const searchIssuesV2Tool: ToolConfig = { type: 'object', properties: { id: { type: 'number', description: 'Issue ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, number: { type: 'number', description: 'Issue number' }, title: { type: 'string', description: 'Title' }, - state: { type: 'string', description: 'State' }, + state: { type: 'string', description: 'State (open or closed)' }, + locked: { type: 'boolean', description: 'Whether issue is locked' }, html_url: { type: 'string', description: 'Web URL' }, + url: { type: 'string', description: 'API URL' }, + repository_url: { type: 'string', description: 'Repository API URL' }, + comments_url: { type: 'string', description: 'Comments API URL' }, body: { type: 'string', description: 'Body text', optional: true }, - user: { type: 'object', description: 'Author' }, - labels: { type: 'array', description: 'Labels' }, - assignees: { type: 'array', description: 'Assignees' }, - created_at: { type: 'string', description: 'Creation date' }, - updated_at: { type: 'string', description: 'Update date' }, - closed_at: { type: 'string', description: 'Close date', optional: true }, + comments: { type: 'number', description: 'Number of comments' }, + score: { type: 'number', description: 'Search relevance score' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + closed_at: { type: 'string', description: 'Close timestamp', optional: true }, + user: { + type: 'object', + description: 'Issue author', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + labels: { + type: 'array', + description: 'Issue labels', + items: { + type: 'object', + properties: { + id: { type: 'number', description: 'Label ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + url: { type: 'string', description: 'API URL' }, + name: { type: 'string', description: 'Label name' }, + description: { type: 'string', description: 'Label description', optional: true }, + color: { type: 'string', description: 'Hex color code' }, + default: { type: 'boolean', description: 'Whether this is a default label' }, + }, + }, + }, + assignee: { + type: 'object', + description: 'Primary assignee', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + assignees: { + type: 'array', + description: 'All assignees', + items: { + type: 'object', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, + }, + milestone: { + type: 'object', + description: 'Associated milestone', + optional: true, + properties: { + id: { type: 'number', description: 'Milestone ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + number: { type: 'number', description: 'Milestone number' }, + title: { type: 'string', description: 'Milestone title' }, + description: { type: 'string', description: 'Milestone description', optional: true }, + state: { type: 'string', description: 'State (open or closed)' }, + html_url: { type: 'string', description: 'Web URL' }, + due_on: { type: 'string', description: 'Due date', optional: true }, + }, + }, + pull_request: { + type: 'object', + description: 'Pull request details (if this is a PR)', + optional: true, + properties: { + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Web URL' }, + diff_url: { type: 'string', description: 'Diff URL' }, + patch_url: { type: 'string', description: 'Patch URL' }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/search_repos.ts b/apps/sim/tools/github/search_repos.ts index 17655c4856..933f7fcfb1 100644 --- a/apps/sim/tools/github/search_repos.ts +++ b/apps/sim/tools/github/search_repos.ts @@ -210,15 +210,50 @@ export const searchReposV2Tool: ToolConfig = { type: 'object', properties: { id: { type: 'number', description: 'Repository ID' }, - full_name: { type: 'string', description: 'Full name' }, - description: { type: 'string', description: 'Description', optional: true }, - html_url: { type: 'string', description: 'Web URL' }, - stargazers_count: { type: 'number', description: 'Stars' }, - forks_count: { type: 'number', description: 'Forks' }, - open_issues_count: { type: 'number', description: 'Open issues' }, - language: { type: 'string', description: 'Language', optional: true }, - topics: { type: 'array', description: 'Topics' }, - owner: { type: 'object', description: 'Owner' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + name: { type: 'string', description: 'Repository name' }, + full_name: { type: 'string', description: 'Full name (owner/repo)' }, + private: { type: 'boolean', description: 'Whether repository is private' }, + description: { type: 'string', description: 'Repository description', optional: true }, + html_url: { type: 'string', description: 'GitHub web URL' }, + url: { type: 'string', description: 'API URL' }, + fork: { type: 'boolean', description: 'Whether this is a fork' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + pushed_at: { type: 'string', description: 'Last push timestamp', optional: true }, + size: { type: 'number', description: 'Repository size in KB' }, + stargazers_count: { type: 'number', description: 'Number of stars' }, + watchers_count: { type: 'number', description: 'Number of watchers' }, + forks_count: { type: 'number', description: 'Number of forks' }, + open_issues_count: { type: 'number', description: 'Number of open issues' }, + language: { type: 'string', description: 'Primary programming language', optional: true }, + default_branch: { type: 'string', description: 'Default branch name' }, + score: { type: 'number', description: 'Search relevance score' }, + topics: { type: 'array', description: 'Repository topics' }, + license: { + type: 'object', + description: 'License information', + optional: true, + properties: { + key: { type: 'string', description: 'License key (e.g., mit)' }, + name: { type: 'string', description: 'License name' }, + spdx_id: { type: 'string', description: 'SPDX identifier' }, + }, + }, + owner: { + type: 'object', + description: 'Repository owner', + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/search_users.ts b/apps/sim/tools/github/search_users.ts index 7e9e1283d4..9be9fa2916 100644 --- a/apps/sim/tools/github/search_users.ts +++ b/apps/sim/tools/github/search_users.ts @@ -181,11 +181,21 @@ export const searchUsersV2Tool: ToolConfig = { type: 'object', properties: { id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, login: { type: 'string', description: 'Username' }, - html_url: { type: 'string', description: 'Profile URL' }, - avatar_url: { type: 'string', description: 'Avatar URL' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + gravatar_id: { type: 'string', description: 'Gravatar ID' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + followers_url: { type: 'string', description: 'Followers API URL' }, + following_url: { type: 'string', description: 'Following API URL' }, + gists_url: { type: 'string', description: 'Gists API URL' }, + starred_url: { type: 'string', description: 'Starred API URL' }, + repos_url: { type: 'string', description: 'Repos API URL' }, + organizations_url: { type: 'string', description: 'Organizations API URL' }, type: { type: 'string', description: 'User or Organization' }, - site_admin: { type: 'boolean', description: 'Is site admin' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + score: { type: 'number', description: 'Search relevance score' }, }, }, }, diff --git a/apps/sim/tools/github/update_gist.ts b/apps/sim/tools/github/update_gist.ts index 4f35fcf860..0bc4f82dfd 100644 --- a/apps/sim/tools/github/update_gist.ts +++ b/apps/sim/tools/github/update_gist.ts @@ -155,10 +155,39 @@ export const updateGistV2Tool: ToolConfig = { outputs: { id: { type: 'string', description: 'Gist ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + url: { type: 'string', description: 'API URL' }, html_url: { type: 'string', description: 'Web URL' }, - description: { type: 'string', description: 'Description', optional: true }, - public: { type: 'boolean', description: 'Is public' }, - updated_at: { type: 'string', description: 'Update date' }, - files: { type: 'object', description: 'Current files' }, + forks_url: { type: 'string', description: 'Forks API URL' }, + commits_url: { type: 'string', description: 'Commits API URL' }, + git_pull_url: { type: 'string', description: 'Git pull URL' }, + git_push_url: { type: 'string', description: 'Git push URL' }, + description: { type: 'string', description: 'Gist description', optional: true }, + public: { type: 'boolean', description: 'Whether gist is public' }, + truncated: { type: 'boolean', description: 'Whether files are truncated' }, + comments: { type: 'number', description: 'Number of comments' }, + comments_url: { type: 'string', description: 'Comments API URL' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + files: { + type: 'object', + description: + 'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content)', + }, + owner: { + type: 'object', + description: 'Gist owner', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, } diff --git a/apps/sim/tools/github/update_milestone.ts b/apps/sim/tools/github/update_milestone.ts index c04b6452af..8b45a30905 100644 --- a/apps/sim/tools/github/update_milestone.ts +++ b/apps/sim/tools/github/update_milestone.ts @@ -174,13 +174,35 @@ export const updateMilestoneV2Tool: ToolConfig = { }, outputs: { + id: { type: 'number', description: 'Milestone ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, number: { type: 'number', description: 'Milestone number' }, - title: { type: 'string', description: 'Title' }, - description: { type: 'string', description: 'Description', optional: true }, - state: { type: 'string', description: 'State' }, - html_url: { type: 'string', description: 'Web URL' }, - due_on: { type: 'string', description: 'Due date', optional: true }, - open_issues: { type: 'number', description: 'Open issues' }, - closed_issues: { type: 'number', description: 'Closed issues' }, + title: { type: 'string', description: 'Milestone title' }, + description: { type: 'string', description: 'Milestone description', optional: true }, + state: { type: 'string', description: 'State (open or closed)' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'GitHub web URL' }, + labels_url: { type: 'string', description: 'Labels API URL' }, + due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true }, + open_issues: { type: 'number', description: 'Number of open issues' }, + closed_issues: { type: 'number', description: 'Number of closed issues' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + updated_at: { type: 'string', description: 'Last update timestamp' }, + closed_at: { type: 'string', description: 'Close timestamp', optional: true }, + creator: { + type: 'object', + description: 'Milestone creator', + optional: true, + properties: { + login: { type: 'string', description: 'Username' }, + id: { type: 'number', description: 'User ID' }, + node_id: { type: 'string', description: 'GraphQL node ID' }, + avatar_url: { type: 'string', description: 'Avatar image URL' }, + url: { type: 'string', description: 'API URL' }, + html_url: { type: 'string', description: 'Profile page URL' }, + type: { type: 'string', description: 'User or Organization' }, + site_admin: { type: 'boolean', description: 'GitHub staff indicator' }, + }, + }, }, } diff --git a/apps/sim/tools/google_drive/copy.ts b/apps/sim/tools/google_drive/copy.ts index 803742e61f..fa28f54690 100644 --- a/apps/sim/tools/google_drive/copy.ts +++ b/apps/sim/tools/google_drive/copy.ts @@ -97,6 +97,7 @@ export const copyTool: ToolConfig = { } as unknown as Record, } }, + + outputs: { + responses: { + type: 'array', + description: 'Array of form responses (when no responseId provided)', + items: { + type: 'object', + properties: { + responseId: { type: 'string', description: 'Unique response ID' }, + createTime: { type: 'string', description: 'When the response was created' }, + lastSubmittedTime: { + type: 'string', + description: 'When the response was last submitted', + }, + answers: { + type: 'json', + description: 'Map of question IDs to answer values', + }, + }, + }, + }, + response: { + type: 'object', + description: 'Single form response (when responseId is provided)', + properties: { + responseId: { type: 'string', description: 'Unique response ID' }, + createTime: { type: 'string', description: 'When the response was created' }, + lastSubmittedTime: { type: 'string', description: 'When the response was last submitted' }, + answers: { + type: 'json', + description: 'Map of question IDs to answer values', + }, + }, + }, + raw: { + type: 'json', + description: 'Raw API response data', + }, + }, } diff --git a/apps/sim/tools/google_slides/create_shape.ts b/apps/sim/tools/google_slides/create_shape.ts index 55a0d675ff..f9575a9358 100644 --- a/apps/sim/tools/google_slides/create_shape.ts +++ b/apps/sim/tools/google_slides/create_shape.ts @@ -351,21 +351,15 @@ export const createShapeTool: ToolConfig description: 'The type of shape that was created', }, metadata: { - type: 'json', + type: 'object', description: 'Operation metadata including presentation ID and page object ID', properties: { - presentationId: { - type: 'string', - description: 'The presentation ID', - }, + presentationId: { type: 'string', description: 'The presentation ID' }, pageObjectId: { type: 'string', description: 'The page object ID where the shape was created', }, - url: { - type: 'string', - description: 'URL to open the presentation', - }, + url: { type: 'string', description: 'URL to the presentation' }, }, }, }, diff --git a/apps/sim/tools/google_slides/create_table.ts b/apps/sim/tools/google_slides/create_table.ts index e3d41dc59a..bb059b6fcd 100644 --- a/apps/sim/tools/google_slides/create_table.ts +++ b/apps/sim/tools/google_slides/create_table.ts @@ -220,21 +220,15 @@ export const createTableTool: ToolConfig description: 'Number of columns in the table', }, metadata: { - type: 'json', + type: 'object', description: 'Operation metadata including presentation ID and page object ID', properties: { - presentationId: { - type: 'string', - description: 'The presentation ID', - }, + presentationId: { type: 'string', description: 'The presentation ID' }, pageObjectId: { type: 'string', description: 'The page object ID where the table was created', }, - url: { - type: 'string', - description: 'URL to open the presentation', - }, + url: { type: 'string', description: 'URL to the presentation' }, }, }, }, diff --git a/apps/sim/tools/google_slides/delete_object.ts b/apps/sim/tools/google_slides/delete_object.ts index 05a6203319..218d7e99bf 100644 --- a/apps/sim/tools/google_slides/delete_object.ts +++ b/apps/sim/tools/google_slides/delete_object.ts @@ -124,17 +124,11 @@ export const deleteObjectTool: ToolConfig = { description: 'The type of page (SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER)', }, pageElements: { - type: 'json', + type: 'array', description: 'Array of page elements (shapes, images, tables, etc.) on this page', + items: { + type: 'json', + }, }, slideProperties: { - type: 'json', + type: 'object', description: 'Properties specific to slides (layout, master, notes)', optional: true, - }, - metadata: { - type: 'json', - description: 'Operation metadata including presentation ID and URL', properties: { - presentationId: { + layoutObjectId: { type: 'string', - description: 'The presentation ID', + description: 'Object ID of the layout this slide is based on', }, - url: { + masterObjectId: { type: 'string', - description: 'URL to open the presentation', + description: 'Object ID of the master this slide is based on', + }, + notesPage: { + type: 'json', + description: 'The notes page associated with the slide', + optional: true, }, }, }, + metadata: { + type: 'object', + description: 'Operation metadata including presentation ID and URL', + properties: { + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, + }, + }, }, } diff --git a/apps/sim/tools/google_slides/insert_text.ts b/apps/sim/tools/google_slides/insert_text.ts index 175c4ba9c9..1524538e5a 100644 --- a/apps/sim/tools/google_slides/insert_text.ts +++ b/apps/sim/tools/google_slides/insert_text.ts @@ -152,17 +152,11 @@ export const insertTextTool: ToolConfig = description: 'The text that was inserted', }, metadata: { - type: 'json', + type: 'object', description: 'Operation metadata including presentation ID and URL', properties: { - presentationId: { - type: 'string', - description: 'The presentation ID', - }, - url: { - type: 'string', - description: 'URL to open the presentation', - }, + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, }, }, }, diff --git a/apps/sim/tools/google_slides/update_slides_position.ts b/apps/sim/tools/google_slides/update_slides_position.ts index 2a0012f772..9215d35e04 100644 --- a/apps/sim/tools/google_slides/update_slides_position.ts +++ b/apps/sim/tools/google_slides/update_slides_position.ts @@ -160,17 +160,11 @@ export const updateSlidesPositionTool: ToolConfig< description: 'The index where the slides were moved to', }, metadata: { - type: 'json', + type: 'object', description: 'Operation metadata including presentation ID and URL', properties: { - presentationId: { - type: 'string', - description: 'The presentation ID', - }, - url: { - type: 'string', - description: 'URL to open the presentation', - }, + presentationId: { type: 'string', description: 'The presentation ID' }, + url: { type: 'string', description: 'URL to the presentation' }, }, }, },