mirror of
https://github.com/simstudioai/sim.git
synced 2026-03-15 03:00:33 -04:00
Compare commits
1 Commits
staging
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33cf0de31e |
@@ -98,7 +98,7 @@ export function FileViewer({
|
||||
file={file}
|
||||
workspaceId={workspaceId}
|
||||
canEdit={canEdit}
|
||||
previewMode={previewMode ?? (showPreview ? 'preview' : 'editor')}
|
||||
previewMode={previewMode ?? (showPreview ? 'split' : 'editor')}
|
||||
autoFocus={autoFocus}
|
||||
onDirtyChange={onDirtyChange}
|
||||
onSaveStatusChange={onSaveStatusChange}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export type { PreviewMode } from './file-viewer'
|
||||
export { FileViewer, isPreviewable, isTextEditable } from './file-viewer'
|
||||
export { RICH_PREVIEWABLE_EXTENSIONS } from './preview-panel'
|
||||
export { PREVIEW_ONLY_EXTENSIONS, RICH_PREVIEWABLE_EXTENSIONS } from './preview-panel'
|
||||
|
||||
@@ -23,6 +23,9 @@ const PREVIEWABLE_EXTENSIONS: Record<string, PreviewType> = {
|
||||
svg: 'svg',
|
||||
}
|
||||
|
||||
/** Extensions that should default to rendered preview (no raw editor). */
|
||||
export const PREVIEW_ONLY_EXTENSIONS = new Set(['html', 'htm', 'svg'])
|
||||
|
||||
/** All extensions that have a rich preview renderer. */
|
||||
export const RICH_PREVIEWABLE_EXTENSIONS = new Set(Object.keys(PREVIEWABLE_EXTENSIONS))
|
||||
|
||||
|
||||
@@ -476,11 +476,10 @@ export function Files() {
|
||||
}, [closeListContextMenu])
|
||||
|
||||
useEffect(() => {
|
||||
const isJustCreated = selectedFileId != null && justCreatedFileIdRef.current === selectedFileId
|
||||
if (justCreatedFileIdRef.current && !isJustCreated) {
|
||||
if (justCreatedFileIdRef.current && selectedFileId !== justCreatedFileIdRef.current) {
|
||||
justCreatedFileIdRef.current = null
|
||||
}
|
||||
setShowPreview(!isJustCreated)
|
||||
setShowPreview(true)
|
||||
}, [selectedFileId])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -522,8 +521,8 @@ export function Files() {
|
||||
...(canPreview
|
||||
? [
|
||||
{
|
||||
label: showPreview ? 'Edit' : 'Preview',
|
||||
icon: showPreview ? Pencil : Eye,
|
||||
label: showPreview ? 'Hide Preview' : 'Preview',
|
||||
icon: Eye,
|
||||
onClick: handleTogglePreview,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Square } from 'lucide-react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@@ -51,11 +51,7 @@ interface ResourceContentProps {
|
||||
* Handles table, file, and workflow resource types with appropriate
|
||||
* embedded rendering for each.
|
||||
*/
|
||||
export const ResourceContent = memo(function ResourceContent({
|
||||
workspaceId,
|
||||
resource,
|
||||
previewMode,
|
||||
}: ResourceContentProps) {
|
||||
export function ResourceContent({ workspaceId, resource, previewMode }: ResourceContentProps) {
|
||||
switch (resource.type) {
|
||||
case 'table':
|
||||
return <Table key={resource.id} workspaceId={workspaceId} tableId={resource.id} embedded />
|
||||
@@ -88,7 +84,7 @@ export const ResourceContent = memo(function ResourceContent({
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface ResourceActionsProps {
|
||||
workspaceId: string
|
||||
@@ -307,12 +303,10 @@ interface EmbeddedWorkflowProps {
|
||||
|
||||
function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) {
|
||||
const workflowExists = useWorkflowRegistry((state) => Boolean(state.workflows[workflowId]))
|
||||
const isMetadataLoaded = useWorkflowRegistry(
|
||||
(state) => state.hydration.phase !== 'idle' && state.hydration.phase !== 'metadata-loading'
|
||||
)
|
||||
const hasLoadError = useWorkflowRegistry(
|
||||
(state) => state.hydration.phase === 'error' && state.hydration.workflowId === workflowId
|
||||
)
|
||||
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
|
||||
const hydrationWorkflowId = useWorkflowRegistry((state) => state.hydration.workflowId)
|
||||
const isMetadataLoaded = hydrationPhase !== 'idle' && hydrationPhase !== 'metadata-loading'
|
||||
const hasLoadError = hydrationPhase === 'error' && hydrationWorkflowId === workflowId
|
||||
|
||||
if (!isMetadataLoaded) return LOADING_SKELETON
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { Columns3, Eye, PanelLeft, Pencil } from '@/components/emcn/icons'
|
||||
import { Columns3, Eye, PanelLeft, Rows3 } from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
|
||||
@@ -36,9 +36,9 @@ const EDGE_ZONE = 40
|
||||
const SCROLL_SPEED = 8
|
||||
|
||||
const PREVIEW_MODE_ICONS = {
|
||||
editor: Columns3,
|
||||
split: Eye,
|
||||
preview: Pencil,
|
||||
editor: Rows3,
|
||||
split: Columns3,
|
||||
preview: Eye,
|
||||
} satisfies Record<PreviewMode, (props: ComponentProps<typeof Eye>) => ReactNode>
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { memo, useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import { RICH_PREVIEWABLE_EXTENSIONS } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import {
|
||||
PREVIEW_ONLY_EXTENSIONS,
|
||||
RICH_PREVIEWABLE_EXTENSIONS,
|
||||
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import type {
|
||||
MothershipResource,
|
||||
MothershipResourceType,
|
||||
@@ -31,7 +34,7 @@ interface MothershipViewProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const MothershipView = memo(function MothershipView({
|
||||
export function MothershipView({
|
||||
workspaceId,
|
||||
chatId,
|
||||
resources,
|
||||
@@ -46,11 +49,12 @@ export const MothershipView = memo(function MothershipView({
|
||||
}: MothershipViewProps) {
|
||||
const active = resources.find((r) => r.id === activeResourceId) ?? resources[0] ?? null
|
||||
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>('preview')
|
||||
const [previewMode, setPreviewMode] = useState<PreviewMode>('split')
|
||||
const handleCyclePreview = useCallback(() => setPreviewMode((m) => PREVIEW_CYCLE[m]), [])
|
||||
|
||||
useEffect(() => {
|
||||
setPreviewMode('preview')
|
||||
const ext = active?.type === 'file' ? getFileExtension(active.title) : ''
|
||||
setPreviewMode(PREVIEW_ONLY_EXTENSIONS.has(ext) ? 'preview' : 'split')
|
||||
}, [active?.id])
|
||||
|
||||
const isActivePreviewable =
|
||||
@@ -95,4 +99,4 @@ export const MothershipView = memo(function MothershipView({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ export function Home({ chatId }: HomeProps = {}) {
|
||||
|
||||
const handleResourceEvent = useCallback(() => {
|
||||
if (isResourceCollapsedRef.current) {
|
||||
/** Auto-collapse sidebar to give resource panel maximum width for immersive experience */
|
||||
const { isCollapsed, toggleCollapsed } = useSidebarStore.getState()
|
||||
if (!isCollapsed) toggleCollapsed()
|
||||
setIsResourceCollapsed(false)
|
||||
|
||||
@@ -682,7 +682,8 @@ export function useChat(
|
||||
readArgs?.path as string | undefined,
|
||||
tc.result.output
|
||||
)
|
||||
if (resource && addResource(resource)) {
|
||||
if (resource) {
|
||||
addResource(resource)
|
||||
onResourceEventRef.current?.()
|
||||
}
|
||||
}
|
||||
@@ -693,21 +694,12 @@ export function useChat(
|
||||
case 'resource_added': {
|
||||
const resource = parsed.resource
|
||||
if (resource?.type && resource?.id) {
|
||||
const wasAdded = addResource(resource)
|
||||
addResource(resource)
|
||||
invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id)
|
||||
|
||||
if (!wasAdded && activeResourceIdRef.current !== resource.id) {
|
||||
setActiveResourceId(resource.id)
|
||||
}
|
||||
onResourceEventRef.current?.()
|
||||
|
||||
if (resource.type === 'workflow') {
|
||||
const wasRegistered = ensureWorkflowInRegistry(
|
||||
resource.id,
|
||||
resource.title,
|
||||
workspaceId
|
||||
)
|
||||
if (wasAdded && wasRegistered) {
|
||||
if (ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)) {
|
||||
useWorkflowRegistry.getState().setActiveWorkflow(resource.id)
|
||||
} else {
|
||||
useWorkflowRegistry.getState().loadWorkflowState(resource.id)
|
||||
|
||||
@@ -57,7 +57,7 @@ export const STATUS_CONFIG: Record<
|
||||
> = {
|
||||
error: { variant: 'red', label: 'Error', color: 'var(--text-error)' },
|
||||
pending: { variant: 'amber', label: 'Pending', color: '#f59e0b' },
|
||||
running: { variant: 'amber', label: 'Running', color: '#f59e0b' },
|
||||
running: { variant: 'green', label: 'Running', color: '#22c55e' },
|
||||
cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316' },
|
||||
info: { variant: 'gray', label: 'Info', color: 'var(--terminal-status-info-color)' },
|
||||
}
|
||||
|
||||
@@ -249,11 +249,7 @@ export function ScheduledTasks() {
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={handleDelete}
|
||||
disabled={deleteSchedule.isPending}
|
||||
>
|
||||
<Button variant='default' onClick={handleDelete} disabled={deleteSchedule.isPending}>
|
||||
{deleteSchedule.isPending ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
@@ -14,11 +14,18 @@ import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
/**
|
||||
* Dropdown option type - can be a simple string or an object with label, id, and optional icon
|
||||
* Dropdown option type - can be a simple string or an object with label, id, and optional icon.
|
||||
* Options with `hidden: true` are excluded from the picker but still resolve for label display,
|
||||
* so existing workflows that reference them continue to work.
|
||||
*/
|
||||
type DropdownOption =
|
||||
| string
|
||||
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }
|
||||
| {
|
||||
label: string
|
||||
id: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the Dropdown component
|
||||
@@ -185,13 +192,12 @@ export const Dropdown = memo(function Dropdown({
|
||||
return fetchedOptions.map((opt) => ({ label: opt.label, id: opt.id }))
|
||||
}, [fetchedOptions])
|
||||
|
||||
const availableOptions = useMemo(() => {
|
||||
const allOptions = useMemo(() => {
|
||||
let opts: DropdownOption[] =
|
||||
fetchOptions && normalizedFetchedOptions.length > 0
|
||||
? normalizedFetchedOptions
|
||||
: evaluatedOptions
|
||||
|
||||
// Merge hydrated option if not already present
|
||||
if (hydratedOption) {
|
||||
const alreadyPresent = opts.some((o) =>
|
||||
typeof o === 'string' ? o === hydratedOption.id : o.id === hydratedOption.id
|
||||
@@ -204,11 +210,12 @@ export const Dropdown = memo(function Dropdown({
|
||||
return opts
|
||||
}, [fetchOptions, normalizedFetchedOptions, evaluatedOptions, hydratedOption])
|
||||
|
||||
/**
|
||||
* Convert dropdown options to Combobox format
|
||||
*/
|
||||
const selectableOptions = useMemo(() => {
|
||||
return allOptions.filter((opt) => typeof opt === 'string' || !opt.hidden)
|
||||
}, [allOptions])
|
||||
|
||||
const comboboxOptions = useMemo((): ComboboxOption[] => {
|
||||
return availableOptions.map((opt) => {
|
||||
return selectableOptions.map((opt) => {
|
||||
if (typeof opt === 'string') {
|
||||
return { label: opt.toLowerCase(), value: opt }
|
||||
}
|
||||
@@ -218,11 +225,16 @@ export const Dropdown = memo(function Dropdown({
|
||||
icon: 'icon' in opt ? opt.icon : undefined,
|
||||
}
|
||||
})
|
||||
}, [availableOptions])
|
||||
}, [selectableOptions])
|
||||
|
||||
const optionMap = useMemo(() => {
|
||||
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))
|
||||
}, [comboboxOptions])
|
||||
return new Map(
|
||||
allOptions.map((opt) => {
|
||||
if (typeof opt === 'string') return [opt, opt.toLowerCase()]
|
||||
return [opt.id, opt.label.toLowerCase()]
|
||||
})
|
||||
)
|
||||
}, [allOptions])
|
||||
|
||||
const defaultOptionValue = useMemo(() => {
|
||||
if (multiSelect) return undefined
|
||||
|
||||
@@ -138,12 +138,9 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
|
||||
{task.id !== 'new' && (
|
||||
<div className='relative flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center'>
|
||||
{isActive && !isCurrentRoute && (
|
||||
<span className='absolute h-[7px] w-[7px] animate-ping rounded-full bg-amber-400 opacity-30 group-hover:hidden' />
|
||||
<span className='absolute h-[7px] w-[7px] animate-ping rounded-full bg-[#33C482] opacity-30 group-hover:hidden' />
|
||||
)}
|
||||
{isActive && !isCurrentRoute && (
|
||||
<span className='absolute h-[7px] w-[7px] rounded-full bg-amber-400 group-hover:hidden' />
|
||||
)}
|
||||
{!isActive && isUnread && !isCurrentRoute && (
|
||||
{(isActive || isUnread) && !isCurrentRoute && (
|
||||
<span className='absolute h-[7px] w-[7px] rounded-full bg-[#33C482] group-hover:hidden' />
|
||||
)}
|
||||
<button
|
||||
@@ -1099,15 +1096,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
tasks.map((task) => (
|
||||
<DropdownMenuItem key={task.id} asChild>
|
||||
<Link href={task.href}>
|
||||
<span className='relative flex-shrink-0'>
|
||||
<Blimp className='h-[16px] w-[16px]' />
|
||||
{task.isActive && (
|
||||
<span className='-bottom-[1px] -right-[1px] absolute h-[6px] w-[6px] rounded-full border border-[var(--surface-1)] bg-amber-400' />
|
||||
)}
|
||||
{!task.isActive && task.isUnread && (
|
||||
<span className='-bottom-[1px] -right-[1px] absolute h-[6px] w-[6px] rounded-full border border-[var(--surface-1)] bg-[#33C482]' />
|
||||
)}
|
||||
</span>
|
||||
<Blimp className='h-[16px] w-[16px]' />
|
||||
<span>{task.name}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -268,15 +268,17 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
type: 'dropdown',
|
||||
mode: 'trigger',
|
||||
options: grainTriggerOptions,
|
||||
value: () => 'grain_webhook',
|
||||
value: () => 'grain_item_added',
|
||||
required: true,
|
||||
},
|
||||
...getTrigger('grain_item_added').subBlocks,
|
||||
...getTrigger('grain_item_updated').subBlocks,
|
||||
...getTrigger('grain_webhook').subBlocks,
|
||||
...getTrigger('grain_recording_created').subBlocks,
|
||||
...getTrigger('grain_recording_updated').subBlocks,
|
||||
...getTrigger('grain_highlight_created').subBlocks,
|
||||
...getTrigger('grain_highlight_updated').subBlocks,
|
||||
...getTrigger('grain_story_created').subBlocks,
|
||||
...getTrigger('grain_webhook').subBlocks,
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
@@ -447,12 +449,14 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`,
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'grain_item_added',
|
||||
'grain_item_updated',
|
||||
'grain_webhook',
|
||||
'grain_recording_created',
|
||||
'grain_recording_updated',
|
||||
'grain_highlight_created',
|
||||
'grain_highlight_updated',
|
||||
'grain_story_created',
|
||||
'grain_webhook',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -233,12 +233,14 @@ export interface SubBlockConfig {
|
||||
id: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
group?: string
|
||||
hidden?: boolean
|
||||
}[]
|
||||
| (() => {
|
||||
label: string
|
||||
id: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
group?: string
|
||||
hidden?: boolean
|
||||
}[])
|
||||
min?: number
|
||||
max?: number
|
||||
|
||||
@@ -40,7 +40,7 @@ export function PlayOutline(props: SVGProps<SVGSVGElement>) {
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
{...props}
|
||||
>
|
||||
<path d='M7.5 3.5C7.5 2.672 8.452 2.18 9.128 2.66L18.128 9.16C18.72 9.58 18.72 10.46 18.128 10.88L9.128 17.38C8.452 17.86 7.5 17.368 7.5 16.54V3.5Z' />
|
||||
<path d='M6.25 3.9C6.25 3.408 6.799 3.114 7.209 3.399L15.709 9.299C16.063 9.545 16.063 10.069 15.709 10.315L7.209 16.215C6.799 16.5 6.25 16.206 6.25 15.714V3.9Z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1258,6 +1258,8 @@ export async function createGrainWebhookSubscription(
|
||||
}
|
||||
|
||||
const actionMap: Record<string, Array<'added' | 'updated' | 'removed'>> = {
|
||||
grain_item_added: ['added'],
|
||||
grain_item_updated: ['updated'],
|
||||
grain_recording_created: ['added'],
|
||||
grain_recording_updated: ['updated'],
|
||||
grain_highlight_created: ['added'],
|
||||
@@ -1267,6 +1269,8 @@ export async function createGrainWebhookSubscription(
|
||||
|
||||
const eventTypeMap: Record<string, string[]> = {
|
||||
grain_webhook: [],
|
||||
grain_item_added: [],
|
||||
grain_item_updated: [],
|
||||
grain_recording_created: ['recording_added'],
|
||||
grain_recording_updated: ['recording_updated'],
|
||||
grain_highlight_created: ['highlight_added'],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export { grainHighlightCreatedTrigger } from './highlight_created'
|
||||
export { grainHighlightUpdatedTrigger } from './highlight_updated'
|
||||
export { grainItemAddedTrigger } from './item_added'
|
||||
export { grainItemUpdatedTrigger } from './item_updated'
|
||||
export { grainRecordingCreatedTrigger } from './recording_created'
|
||||
export { grainRecordingUpdatedTrigger } from './recording_updated'
|
||||
export { grainStoryCreatedTrigger } from './story_created'
|
||||
|
||||
76
apps/sim/triggers/grain/item_added.ts
Normal file
76
apps/sim/triggers/grain/item_added.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { GrainIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
|
||||
|
||||
export const grainItemAddedTrigger: TriggerConfig = {
|
||||
id: 'grain_item_added',
|
||||
name: 'Grain Item Added',
|
||||
provider: 'grain',
|
||||
description: 'Trigger when a new item is added to a Grain view (recording, highlight, or story)',
|
||||
version: '1.0.0',
|
||||
icon: GrainIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Grain API key (Personal Access Token)',
|
||||
description: 'Required to create the webhook in Grain.',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_added',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'viewId',
|
||||
title: 'View ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Grain view UUID',
|
||||
description:
|
||||
'The view determines which content type fires events (recordings, highlights, or stories).',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_added',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'grain_item_added',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_added',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: grainV2SetupInstructions('item added'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_added',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildGenericOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
76
apps/sim/triggers/grain/item_updated.ts
Normal file
76
apps/sim/triggers/grain/item_updated.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { GrainIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
|
||||
|
||||
export const grainItemUpdatedTrigger: TriggerConfig = {
|
||||
id: 'grain_item_updated',
|
||||
name: 'Grain Item Updated',
|
||||
provider: 'grain',
|
||||
description: 'Trigger when an item is updated in a Grain view (recording, highlight, or story)',
|
||||
version: '1.0.0',
|
||||
icon: GrainIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Grain API key (Personal Access Token)',
|
||||
description: 'Required to create the webhook in Grain.',
|
||||
password: true,
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'viewId',
|
||||
title: 'View ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Grain view UUID',
|
||||
description:
|
||||
'The view determines which content type fires events (recordings, highlights, or stories).',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
type: 'trigger-save',
|
||||
hideFromPreview: true,
|
||||
mode: 'trigger',
|
||||
triggerId: 'grain_item_updated',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_updated',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: grainV2SetupInstructions('item updated'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
value: 'grain_item_updated',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
outputs: buildGenericOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import type { TriggerOutput } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Shared trigger dropdown options for all Grain triggers
|
||||
* Trigger dropdown options for Grain triggers.
|
||||
* New options (Item Added / Item Updated / All Events) correctly scope by view_id only.
|
||||
* Legacy options are hidden from the picker but still resolve for existing workflows.
|
||||
*/
|
||||
export const grainTriggerOptions = [
|
||||
{ label: 'General Webhook (All Events)', id: 'grain_webhook' },
|
||||
{ label: 'Recording Created', id: 'grain_recording_created' },
|
||||
{ label: 'Recording Updated', id: 'grain_recording_updated' },
|
||||
{ label: 'Highlight Created', id: 'grain_highlight_created' },
|
||||
{ label: 'Highlight Updated', id: 'grain_highlight_updated' },
|
||||
{ label: 'Story Created', id: 'grain_story_created' },
|
||||
{ label: 'Item Added', id: 'grain_item_added' },
|
||||
{ label: 'Item Updated', id: 'grain_item_updated' },
|
||||
{ label: 'All Events', id: 'grain_webhook' },
|
||||
{ label: 'Recording Created', id: 'grain_recording_created', hidden: true },
|
||||
{ label: 'Recording Updated', id: 'grain_recording_updated', hidden: true },
|
||||
{ label: 'Highlight Created', id: 'grain_highlight_created', hidden: true },
|
||||
{ label: 'Highlight Updated', id: 'grain_highlight_updated', hidden: true },
|
||||
{ label: 'Story Created', id: 'grain_story_created', hidden: true },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -32,6 +36,25 @@ export function grainSetupInstructions(eventType: string): string {
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup instructions for the v2 triggers that correctly explain view-based scoping.
|
||||
*/
|
||||
export function grainV2SetupInstructions(action: string): string {
|
||||
const instructions = [
|
||||
'Enter your Grain API Key (Personal Access Token). You can find or create one in Grain at <strong>Workspace Settings > API</strong> under Integrations on <a href="https://grain.com/app/settings/integrations?tab=api" target="_blank" rel="noopener noreferrer">grain.com</a>.',
|
||||
`Enter a Grain <strong>view ID</strong>. Each view has a type — <em>recordings</em>, <em>highlights</em>, or <em>stories</em> — and only items matching that type will fire the <strong>${action}</strong> event.`,
|
||||
'To find your view IDs, use the <strong>List Views</strong> operation on this block or call <code>GET /_/public-api/views</code> directly.',
|
||||
'The webhook is created automatically when you save and will be deleted when you remove this trigger.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build output schema for recording events
|
||||
* Webhook payload structure: { type, user_id, data: { ...recording } }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { GrainIcon } from '@/components/icons'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
import { buildGenericOutputs, grainSetupInstructions } from './utils'
|
||||
import { buildGenericOutputs, grainV2SetupInstructions } from './utils'
|
||||
|
||||
export const grainWebhookTrigger: TriggerConfig = {
|
||||
id: 'grain_webhook',
|
||||
name: 'Grain Webhook',
|
||||
name: 'Grain All Events',
|
||||
provider: 'grain',
|
||||
description: 'Generic webhook trigger for all actions in a selected Grain view',
|
||||
description: 'Trigger on all actions (added, updated, removed) in a Grain view',
|
||||
version: '1.0.0',
|
||||
icon: GrainIcon,
|
||||
|
||||
@@ -30,7 +30,8 @@ export const grainWebhookTrigger: TriggerConfig = {
|
||||
title: 'View ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter Grain view UUID',
|
||||
description: 'Required by Grain to create the webhook subscription.',
|
||||
description:
|
||||
'The view determines which content type fires events (recordings, highlights, or stories).',
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
@@ -55,7 +56,7 @@ export const grainWebhookTrigger: TriggerConfig = {
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: grainSetupInstructions('All events'),
|
||||
defaultValue: grainV2SetupInstructions('all'),
|
||||
mode: 'trigger',
|
||||
condition: {
|
||||
field: 'selectedTriggerId',
|
||||
|
||||
@@ -89,6 +89,8 @@ import { googleFormsWebhookTrigger } from '@/triggers/googleforms'
|
||||
import {
|
||||
grainHighlightCreatedTrigger,
|
||||
grainHighlightUpdatedTrigger,
|
||||
grainItemAddedTrigger,
|
||||
grainItemUpdatedTrigger,
|
||||
grainRecordingCreatedTrigger,
|
||||
grainRecordingUpdatedTrigger,
|
||||
grainStoryCreatedTrigger,
|
||||
@@ -245,6 +247,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
fathom_webhook: fathomWebhookTrigger,
|
||||
gmail_poller: gmailPollingTrigger,
|
||||
grain_webhook: grainWebhookTrigger,
|
||||
grain_item_added: grainItemAddedTrigger,
|
||||
grain_item_updated: grainItemUpdatedTrigger,
|
||||
grain_recording_created: grainRecordingCreatedTrigger,
|
||||
grain_recording_updated: grainRecordingUpdatedTrigger,
|
||||
grain_highlight_created: grainHighlightCreatedTrigger,
|
||||
|
||||
Reference in New Issue
Block a user