chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import { createElement, type SVGProps } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { BlockConfig } from '@/blocks/types'
const { mockGetAllBlocks, mockGetToolOperationsIndex, mockGetTriggersForSidebar } = vi.hoisted(
() => ({
mockGetAllBlocks: vi.fn(),
mockGetToolOperationsIndex: vi.fn(() => []),
mockGetTriggersForSidebar: vi.fn(() => []),
})
)
vi.mock('@/blocks', () => ({
getAllBlocks: mockGetAllBlocks,
}))
vi.mock('@/lib/search/tool-operations', () => ({
getToolOperationsIndex: mockGetToolOperationsIndex,
}))
vi.mock('@/lib/workflows/triggers/trigger-utils', () => ({
getTriggersForSidebar: mockGetTriggersForSidebar,
}))
import {
buildCommandSearchableOptionSearchValue,
useSearchModalStore,
} from '@/stores/modals/search/store'
function TestIcon(props: SVGProps<SVGSVGElement>) {
return createElement('svg', props)
}
function createBlock(overrides: Partial<BlockConfig> = {}): BlockConfig {
return {
type: 'image_generator_v2',
name: 'Image Generator',
description: 'Generate images',
category: 'tools',
bgColor: '#4D5FFF',
icon: TestIcon,
subBlocks: [
{
id: 'provider',
title: 'Provider',
type: 'dropdown',
commandSearchable: true,
options: [
{ label: 'OpenAI', id: 'openai' },
{ label: 'Fal.ai (Multi-Model)', id: 'falai' },
{ label: 'Hidden Provider', id: 'hidden', hidden: true },
],
},
],
tools: { access: ['image_generate'] },
inputs: {},
outputs: {},
...overrides,
}
}
describe('search modal store', () => {
beforeEach(() => {
vi.clearAllMocks()
useSearchModalStore.setState({
isOpen: false,
data: {
blocks: [],
tools: [],
triggers: [],
toolOperations: [],
docs: [],
isInitialized: false,
},
})
})
describe('buildCommandSearchableOptionSearchValue', () => {
it('builds search terms for marked static dropdown options', () => {
const block = createBlock()
const searchValue = buildCommandSearchableOptionSearchValue(block)
expect(searchValue).toContain('Provider')
expect(searchValue).toContain('Fal.ai (Multi-Model)')
expect(searchValue).toContain('falai')
expect(searchValue).not.toContain('Hidden Provider')
expect(searchValue).not.toContain('hidden')
})
it('does not index dropdowns that only use in-dropdown search', () => {
const block = createBlock({
subBlocks: [
{
id: 'timezone',
title: 'Timezone',
type: 'dropdown',
searchable: true,
options: [{ label: 'UTC', id: 'utc' }],
},
],
})
expect(buildCommandSearchableOptionSearchValue(block)).toBe('')
})
it('builds search terms for marked combobox option functions', () => {
const block = createBlock({
subBlocks: [
{
id: 'model',
title: 'Model',
type: 'combobox',
commandSearchable: true,
options: () => [
{ label: 'claude-sonnet-4-6', id: 'claude-sonnet-4-6' },
{ label: 'Hidden Model', id: 'hidden-model', hidden: true },
],
},
],
})
const searchValue = buildCommandSearchableOptionSearchValue(block)
expect(searchValue).toContain('Model')
expect(searchValue).toContain('claude-sonnet-4-6')
expect(searchValue).not.toContain('Hidden Model')
expect(searchValue).not.toContain('hidden-model')
})
})
it('adds command-searchable options to visible block search values without extra rows', () => {
const visibleBlock = createBlock()
const hiddenBlock = createBlock({
type: 'hidden_generator',
hideFromToolbar: true,
})
mockGetAllBlocks.mockReturnValue([visibleBlock, hiddenBlock])
useSearchModalStore.getState().initializeData((blocks) => blocks)
const { tools } = useSearchModalStore.getState().data
expect(tools).toHaveLength(1)
expect(tools[0]).toEqual(
expect.objectContaining({
id: 'image_generator_v2',
searchValue: expect.stringContaining('Fal.ai (Multi-Model)'),
})
)
})
})
+204
View File
@@ -0,0 +1,204 @@
import { RepeatIcon, SplitIcon } from 'lucide-react'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { getToolOperationsIndex } from '@/lib/search/tool-operations'
import { getTriggersForSidebar } from '@/lib/workflows/triggers/trigger-utils'
import { getAllBlocks } from '@/blocks'
import type { BlockConfig, SubBlockConfig } from '@/blocks/types'
import type {
SearchBlockItem,
SearchData,
SearchDocItem,
SearchModalState,
SearchToolOperationItem,
} from './types'
const initialData: SearchData = {
blocks: [],
tools: [],
triggers: [],
toolOperations: [],
docs: [],
isInitialized: false,
}
type CommandSearchableOption = {
label: string
id: string
hidden?: boolean
}
function getCommandSearchableOptions(subBlock: SubBlockConfig): CommandSearchableOption[] {
if (!subBlock.options) return []
try {
const options = typeof subBlock.options === 'function' ? subBlock.options() : subBlock.options
return Array.isArray(options) ? options : []
} catch {
return []
}
}
export function buildCommandSearchableOptionSearchValue(block: BlockConfig): string {
const terms = new Set<string>()
for (const subBlock of block.subBlocks) {
if (
(subBlock.type !== 'dropdown' && subBlock.type !== 'combobox') ||
!subBlock.commandSearchable
) {
continue
}
for (const option of getCommandSearchableOptions(subBlock)) {
if (option.hidden) continue
const subBlockTitle = subBlock.title ?? subBlock.id
terms.add(subBlockTitle)
terms.add(option.label)
terms.add(option.id)
}
}
return Array.from(terms).join(' ')
}
export const useSearchModalStore = create<SearchModalState>()(
devtools(
(set, _) => ({
isOpen: false,
sections: null,
pendingConnect: null,
data: initialData,
setOpen: (open: boolean) => {
set({ isOpen: open, sections: null, pendingConnect: null })
},
open: (options) => {
set({
isOpen: true,
sections: options?.sections ?? null,
pendingConnect: options?.pendingConnect ?? null,
})
},
close: () => {
set({ isOpen: false, sections: null, pendingConnect: null })
},
initializeData: (filterBlocks) => {
const allBlocks = getAllBlocks()
const filteredAllBlocks = filterBlocks(allBlocks) as typeof allBlocks
const regularBlocks: SearchBlockItem[] = []
const tools: SearchBlockItem[] = []
const docs: SearchDocItem[] = []
for (const block of filteredAllBlocks) {
if (block.hideFromToolbar) continue
const searchItem: SearchBlockItem = {
id: block.type,
name: block.name,
icon: block.icon,
bgColor: block.bgColor || '#6B7280',
type: block.type,
searchValue: `${block.name} ${block.type} ${buildCommandSearchableOptionSearchValue(block)}`,
}
if (block.category === 'blocks' && block.type !== 'starter') {
regularBlocks.push(searchItem)
} else if (block.category === 'tools') {
tools.push(searchItem)
}
if (block.docsLink) {
docs.push({
id: `docs-${block.type}`,
name: block.name,
icon: block.icon,
href: block.docsLink,
})
}
}
const specialBlocks: SearchBlockItem[] = [
{
id: 'loop',
name: 'Loop',
icon: RepeatIcon,
bgColor: '#2FB3FF',
type: 'loop',
},
{
id: 'parallel',
name: 'Parallel',
icon: SplitIcon,
bgColor: '#FEE12B',
type: 'parallel',
},
]
const blocks = [...regularBlocks, ...(filterBlocks(specialBlocks) as SearchBlockItem[])]
const allTriggers = getTriggersForSidebar()
const filteredTriggers = filterBlocks(allTriggers) as typeof allTriggers
const priorityOrder = ['Start', 'Schedule', 'Webhook']
const sortedTriggers = [...filteredTriggers].sort(
(a: (typeof filteredTriggers)[number], b: (typeof filteredTriggers)[number]) => {
const aIndex = priorityOrder.indexOf(a.name)
const bIndex = priorityOrder.indexOf(b.name)
const aHasPriority = aIndex !== -1
const bHasPriority = bIndex !== -1
if (aHasPriority && bHasPriority) return aIndex - bIndex
if (aHasPriority) return -1
if (bHasPriority) return 1
return a.name.localeCompare(b.name)
}
)
const triggers = sortedTriggers.map(
(block): SearchBlockItem => ({
id: block.type,
name: block.name,
icon: block.icon,
bgColor: block.bgColor || '#6B7280',
type: block.type,
config: block,
})
)
const allowedBlockTypes = new Set(tools.map((t) => t.type))
const toolOperations: SearchToolOperationItem[] = getToolOperationsIndex()
.filter((op) => allowedBlockTypes.has(op.blockType))
.map((op) => {
const aliasesStr = op.aliases?.length ? ` ${op.aliases.join(' ')}` : ''
return {
id: op.id,
name: op.operationName,
searchValue: `${op.serviceName} ${op.operationName}${aliasesStr}`,
icon: op.icon,
bgColor: op.bgColor,
blockType: op.blockType,
operationId: op.operationId,
}
})
set({
data: {
blocks,
tools,
triggers,
toolOperations,
docs,
isInitialized: true,
},
})
},
}),
{ name: 'search-modal-store' }
)
)
+138
View File
@@ -0,0 +1,138 @@
import type { ComponentType } from 'react'
import type { BlockConfig } from '@/blocks/types'
/**
* Represents a block item in the search results.
*/
export interface SearchBlockItem {
id: string
name: string
icon: ComponentType<{ className?: string }>
bgColor: string
type: string
config?: BlockConfig
searchValue?: string
}
/**
* Represents a tool operation item in the search results.
*/
export interface SearchToolOperationItem {
id: string
name: string
searchValue: string
icon: ComponentType<{ className?: string }>
bgColor: string
blockType: string
operationId: string
}
/**
* Represents a doc item in the search results.
*/
export interface SearchDocItem {
id: string
name: string
icon: ComponentType<{ className?: string }>
href: string
}
/**
* Pre-computed search data that is initialized on app load.
*/
export interface SearchData {
blocks: SearchBlockItem[]
tools: SearchBlockItem[]
triggers: SearchBlockItem[]
toolOperations: SearchToolOperationItem[]
docs: SearchDocItem[]
isInitialized: boolean
}
/**
* Every result group the search modal can render, in render order. Used to
* restrict the palette to a subset of sections when opened for a specific
* intent (e.g. a drag-release that should only offer canvas-insertable items).
*/
export const SEARCH_SECTIONS = [
'actions',
'connectedAccounts',
'integrations',
'blocks',
'tools',
'triggers',
'chats',
'workflows',
'tables',
'files',
'knowledgeBases',
'toolOperations',
'workspaces',
'docs',
'pages',
] as const
/** A single search-modal result group. */
export type SearchSection = (typeof SEARCH_SECTIONS)[number]
/**
* Context handed to the palette when it is opened to complete an edge
* drag-release: the dragged source handle and the release point. A selection
* stamps it onto its event so the canvas places the block at the drop point and
* wires it from that handle.
*/
export interface PendingConnect {
source: { nodeId: string; handleId: string }
screenX: number
screenY: number
}
/**
* Global state for the universal search modal.
*
* Centralizing this state in a store allows any component (e.g. sidebar,
* workflow command list, keyboard shortcuts) to open or close the modal
* without relying on DOM events or prop drilling.
*/
export interface SearchModalState {
/** Whether the search modal is currently open. */
isOpen: boolean
/**
* When set, the palette renders only these sections; `null` shows all of them.
*/
sections: SearchSection[] | null
/**
* Pending edge drag-release the palette was opened to complete. A selection
* stamps it onto its event; other add-block dispatchers carry none, so only a
* genuine palette pick completes the connection. `null` for ordinary opens.
*/
pendingConnect: PendingConnect | null
/** Pre-computed search data. */
data: SearchData
/**
* Explicitly set the open state of the modal. Always resets to the full
* palette (no section restriction, no pending connect).
*/
setOpen: (open: boolean) => void
/**
* Convenience method to open the modal. Pass `sections` to restrict the
* palette to a subset of result groups, and `pendingConnect` to complete an
* edge drag-release with the selection.
*/
open: (options?: { sections?: SearchSection[]; pendingConnect?: PendingConnect }) => void
/**
* Convenience method to close the modal.
*/
close: () => void
/**
* Initialize search data. Called once on app load.
*/
initializeData: (filterBlocks: <T extends { type: string }>(blocks: T[]) => T[]) => void
}