mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(variables): fixed floating variables, added autcomplete off to prevent suggested values (#1908)
* fixed variables * add autocomplete=off to prevent suggestions for subblock values
This commit is contained in:
+3
-2
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Loader2, Trash2 } from 'lucide-react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -97,7 +98,7 @@ export function DeleteChunkModal({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className='mr-2 h-4 w-4' />
|
||||
<Trash className='mr-2 h-4 w-4' />
|
||||
Delete
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, startTransition, useCallback, useEffect, useState } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Circle, CircleOff, FileText, Plus, Trash2 } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Circle, CircleOff, FileText, Plus } from 'lucide-react'
|
||||
import { useParams, useSearchParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button, Checkbox, SearchHighlight } from '@/components/ui'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
@@ -369,7 +370,7 @@ export function Document({
|
||||
disabled={!userPermissions.canEdit}
|
||||
className='h-8 w-8 p-0 text-gray-500 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>Delete Chunk</Tooltip.Content>
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
Loader2,
|
||||
Plus,
|
||||
RotateCcw,
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -1070,7 +1070,7 @@ export function KnowledgeBase({
|
||||
}
|
||||
className='h-8 w-8 p-0 text-gray-500 hover:text-red-600 disabled:opacity-50'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top'>
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { LibraryBig, MoreHorizontal, Trash2 } from 'lucide-react'
|
||||
import { LibraryBig, MoreHorizontal } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -107,7 +108,7 @@ export function KnowledgeHeader({ breadcrumbs, options }: KnowledgeHeaderProps)
|
||||
onClick={options.onDeleteKnowledgeBase}
|
||||
className='flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 font-[380] text-red-600 text-sm hover:bg-secondary/50 focus:bg-secondary/50 focus:text-red-600'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-4 w-4' />
|
||||
Delete Knowledge Base
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw, Trash2 } from 'lucide-react'
|
||||
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button, Card, CardContent, Input, Label } from '@/components/ui'
|
||||
import { getEnv, isTruthy } from '@/lib/env'
|
||||
import { cn, generatePassword } from '@/lib/utils'
|
||||
@@ -266,7 +267,7 @@ export function AuthSelector({
|
||||
disabled={disabled}
|
||||
className='h-7 w-7 opacity-70'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
+3
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { History, Plus, Trash2 } from 'lucide-react'
|
||||
import { History, Plus } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PopoverSection,
|
||||
PopoverTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
CopilotMessage,
|
||||
@@ -387,7 +388,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
|
||||
}}
|
||||
aria-label='Delete chat'
|
||||
>
|
||||
<Trash2 className='h-[10px] w-[10px]' />
|
||||
<Trash className='h-[10px] w-[10px]' />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverItem>
|
||||
|
||||
+6
-2
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { MAX_TAG_SLOTS } from '@/lib/knowledge/consts'
|
||||
@@ -301,6 +302,7 @@ export function DocumentTagEntry({
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
autoComplete='off'
|
||||
className={cn(
|
||||
'w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
isDuplicate && 'border-red-500 bg-red-50'
|
||||
@@ -393,6 +395,7 @@ export function DocumentTagEntry({
|
||||
value={cellValue}
|
||||
readOnly
|
||||
disabled={disabled || isReadOnly}
|
||||
autoComplete='off'
|
||||
className='w-full cursor-pointer border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
onClick={handleTypeDropdownClick}
|
||||
onFocus={handleTypeFocus}
|
||||
@@ -464,6 +467,7 @@ export function DocumentTagEntry({
|
||||
onDrop={handlers.onDrop}
|
||||
onDragOver={handlers.onDragOver}
|
||||
disabled={disabled}
|
||||
autoComplete='off'
|
||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
@@ -507,7 +511,7 @@ export function DocumentTagEntry({
|
||||
className='-translate-y-1/2 absolute top-1/2 right-2 h-8 w-8 opacity-0 group-hover:opacity-100'
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
>
|
||||
<Trash2 className='h-4 w-4 text-muted-foreground' />
|
||||
<Trash className='h-4 w-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</td>
|
||||
) : null
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { Plus, Trash } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Tooltip } from '@/components/emcn'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import { Input } from '@/components/emcn/components/input/input'
|
||||
import { Textarea } from '@/components/emcn/components/textarea/textarea'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/formatted-text'
|
||||
|
||||
+5
-2
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -234,6 +235,7 @@ export function KnowledgeTagFilters({
|
||||
value={cellValue}
|
||||
readOnly
|
||||
disabled={disabled || isLoading}
|
||||
autoComplete='off'
|
||||
className='w-full cursor-pointer border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
onClick={handleDropdownClick}
|
||||
onFocus={handleFocus}
|
||||
@@ -321,6 +323,7 @@ export function KnowledgeTagFilters({
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
autoComplete='off'
|
||||
className='w-full border-0 text-transparent caret-foreground placeholder:text-muted-foreground/50 focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
|
||||
@@ -347,7 +350,7 @@ export function KnowledgeTagFilters({
|
||||
className='-translate-y-1/2 absolute top-1/2 right-2 h-8 w-8 opacity-0 group-hover:opacity-100'
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
>
|
||||
<Trash2 className='h-4 w-4 text-muted-foreground' />
|
||||
<Trash className='h-4 w-4 text-muted-foreground' />
|
||||
</Button>
|
||||
</td>
|
||||
) : null
|
||||
|
||||
+1
@@ -113,6 +113,7 @@ function McpInputWithTags({
|
||||
onDragOver={handleDragOver}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
autoComplete='off'
|
||||
className={cn(!isPassword && 'text-transparent caret-foreground')}
|
||||
/>
|
||||
{!isPassword && (
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react'
|
||||
import { Plus, Trash } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import 'prismjs/components/prism-json'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import { Badge, Button, Combobox, Input } from '@/components/emcn'
|
||||
@@ -330,6 +331,7 @@ export function FieldFormat({
|
||||
}
|
||||
placeholder={valuePlaceholder}
|
||||
disabled={isReadOnly}
|
||||
autoComplete='off'
|
||||
className={cn('allow-scroll w-full overflow-auto', inputClassName)}
|
||||
style={{ overflowX: 'auto' }}
|
||||
/>
|
||||
@@ -378,6 +380,7 @@ export function FieldFormat({
|
||||
onChange={(e) => updateField(field.id, 'name', e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={isReadOnly}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Button } from '@/components/emcn/components/button/button'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -202,6 +202,7 @@ export function Table({
|
||||
onDrop={handlers.onDrop}
|
||||
onDragOver={handlers.onDragOver}
|
||||
disabled={isPreview || disabled}
|
||||
autoComplete='off'
|
||||
className='w-full border-0 bg-transparent px-[10px] py-[8px] text-transparent caret-white placeholder:text-[#787878] focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
<div
|
||||
@@ -262,7 +263,7 @@ export function Table({
|
||||
className='-translate-y-1/2 absolute top-1/2 right-[8px] opacity-0 transition-opacity group-hover:opacity-100'
|
||||
onClick={() => handleDeleteRow(rowIndex)}
|
||||
>
|
||||
<Trash2 className='h-[14px] w-[14px]' />
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
</Button>
|
||||
</td>
|
||||
)
|
||||
|
||||
+3
@@ -94,6 +94,7 @@ export function TimeInput({
|
||||
disabled={isPreview || disabled}
|
||||
value={value ? formatDisplayTime(value) : ''}
|
||||
placeholder={placeholder || 'Select time'}
|
||||
autoComplete='off'
|
||||
className={cn('cursor-pointer', !value && 'text-muted-foreground', className)}
|
||||
/>
|
||||
</div>
|
||||
@@ -122,6 +123,7 @@ export function TimeInput({
|
||||
updateTime(numVal.toString())
|
||||
}}
|
||||
type='text'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<span className='text-[#E6E6E6]'>:</span>
|
||||
<Input
|
||||
@@ -146,6 +148,7 @@ export function TimeInput({
|
||||
updateTime(undefined, numVal.toString())
|
||||
}}
|
||||
type='text'
|
||||
autoComplete='off'
|
||||
/>
|
||||
<Button
|
||||
variant='outline'
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AlertCircle, Code, FileJson, Trash2, X } from 'lucide-react'
|
||||
import { AlertCircle, Code, FileJson, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -1143,7 +1144,7 @@ try {
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className='gap-1'
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-4 w-4' />
|
||||
Delete
|
||||
</Button>
|
||||
) : (
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { Plus, Trash } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
@@ -407,6 +408,7 @@ export function VariablesInput({
|
||||
}
|
||||
placeholder={`${assignment.type} value`}
|
||||
disabled={isPreview || disabled}
|
||||
autoComplete='off'
|
||||
className={cn(
|
||||
'h-9 border border-input bg-white text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background',
|
||||
dragHighlight[assignment.id] && 'ring-2 ring-blue-500 ring-offset-2'
|
||||
|
||||
+1
@@ -82,6 +82,7 @@ export function WebhookConfigField({
|
||||
value={value}
|
||||
onChange={onChange ? (e) => onChange(e.target.value) : undefined}
|
||||
placeholder={placeholder}
|
||||
autoComplete='off'
|
||||
className={cn(
|
||||
'h-10 flex-1',
|
||||
readOnly ? 'cursor-text font-mono text-xs' : '',
|
||||
|
||||
+10
-8
@@ -25,15 +25,15 @@ import {
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables'
|
||||
import { useDeleteWorkflow } from '@/app/workspace/[workspaceId]/w/hooks'
|
||||
import { useChatStore } from '@/stores/chat/store'
|
||||
import { usePanelStore } from '@/stores/panel-new/store'
|
||||
import type { PanelTab } from '@/stores/panel-new/types'
|
||||
// import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useWorkflowJsonStore } from '@/stores/workflows/json/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
// import { Variables } from '../variables/variables'
|
||||
import { Copilot, Deploy, Editor, Toolbar } from './components'
|
||||
import { usePanelResize, useRunWorkflow, useUsageLimits } from './hooks'
|
||||
|
||||
@@ -107,7 +107,7 @@ export function Panel() {
|
||||
|
||||
// Chat state
|
||||
const { isChatOpen, setIsChatOpen } = useChatStore()
|
||||
// const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore()
|
||||
const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore()
|
||||
|
||||
const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null
|
||||
|
||||
@@ -285,10 +285,12 @@ export function Panel() {
|
||||
<Layout className='h-3 w-3' animate={isAutoLayouting} variant='clockwise' />
|
||||
<span>Auto layout</span>
|
||||
</PopoverItem>
|
||||
{/* <PopoverItem onClick={() => setVariablesOpen(!isVariablesOpen)}>
|
||||
<Braces className='h-3 w-3' />
|
||||
<span>Variables</span>
|
||||
</PopoverItem> */}
|
||||
{
|
||||
<PopoverItem onClick={() => setVariablesOpen(!isVariablesOpen)}>
|
||||
<Braces className='h-3 w-3' />
|
||||
<span>Variables</span>
|
||||
</PopoverItem>
|
||||
}
|
||||
{/* <PopoverItem>
|
||||
<Bug className='h-3 w-3' />
|
||||
<span>Debug</span>
|
||||
@@ -469,7 +471,7 @@ export function Panel() {
|
||||
</Modal>
|
||||
|
||||
{/* Floating Variables Modal */}
|
||||
{/* <Variables /> */}
|
||||
<Variables />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+257
-220
@@ -1,8 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Plus, Trash, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Plus, X } from 'lucide-react'
|
||||
import Editor from 'react-simple-code-editor'
|
||||
import type { ComboboxOption } from '@/components/emcn'
|
||||
import {
|
||||
@@ -16,16 +15,16 @@ import {
|
||||
Input,
|
||||
languages,
|
||||
} from '@/components/emcn'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { Label } from '@/components/emcn/components/label/label'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { cn, validateName } from '@/lib/utils'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useVariablesStore as usePanelVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { getVariablesPosition, useVariablesStore } from '@/stores/variables/store'
|
||||
import type { Variable } from '@/stores/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useChatBoundarySync, useChatDrag, useChatResize } from '../chat/hooks'
|
||||
|
||||
const logger = createLogger('FloatingVariables')
|
||||
|
||||
/**
|
||||
* Type options for variable type selection
|
||||
*/
|
||||
@@ -37,6 +36,40 @@ const TYPE_OPTIONS: ComboboxOption[] = [
|
||||
{ label: 'Array', value: 'array' },
|
||||
]
|
||||
|
||||
/**
|
||||
* UI constants for consistent styling and sizing
|
||||
*/
|
||||
const BADGE_HEIGHT = 20
|
||||
const BADGE_TEXT_SIZE = 13
|
||||
const ICON_SIZE = 14
|
||||
const HEADER_ICON_SIZE = 16
|
||||
const LINE_HEIGHT = 21
|
||||
const MIN_EDITOR_HEIGHT = 120
|
||||
|
||||
/**
|
||||
* User-facing strings for errors, labels, and placeholders
|
||||
*/
|
||||
const STRINGS = {
|
||||
errors: {
|
||||
emptyName: 'Variable name cannot be empty',
|
||||
duplicateName: 'Two variables cannot have the same name',
|
||||
},
|
||||
labels: {
|
||||
name: 'Name',
|
||||
type: 'Type',
|
||||
value: 'Value',
|
||||
},
|
||||
placeholders: {
|
||||
name: 'variableName',
|
||||
number: '42',
|
||||
boolean: 'true',
|
||||
plain: 'Plain text value',
|
||||
object: '{\n "key": "value"\n}',
|
||||
array: '[\n 1, 2, 3\n]',
|
||||
},
|
||||
emptyState: 'No variables yet',
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating Variables modal component
|
||||
*
|
||||
@@ -46,29 +79,18 @@ const TYPE_OPTIONS: ComboboxOption[] = [
|
||||
* - Uses emcn Input/Code/Combobox components for a consistent UI
|
||||
*/
|
||||
export function Variables() {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
const { activeWorkflowId } = useWorkflowRegistry()
|
||||
|
||||
// UI store
|
||||
const {
|
||||
isOpen,
|
||||
position,
|
||||
width,
|
||||
height,
|
||||
setIsOpen,
|
||||
setPosition,
|
||||
setDimensions,
|
||||
// Data
|
||||
variables,
|
||||
loadForWorkflow,
|
||||
addVariable,
|
||||
updateVariable,
|
||||
deleteVariable,
|
||||
getVariablesByWorkflowId,
|
||||
} = useVariablesStore()
|
||||
const { isOpen, position, width, height, setIsOpen, setPosition, setDimensions } =
|
||||
useVariablesStore()
|
||||
|
||||
const { getVariablesByWorkflowId } = usePanelVariablesStore()
|
||||
|
||||
const { collaborativeUpdateVariable, collaborativeAddVariable, collaborativeDeleteVariable } =
|
||||
useCollaborativeWorkflow()
|
||||
|
||||
const workflowVariables = activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []
|
||||
|
||||
// Local UI helpers
|
||||
const actualPosition = useMemo(
|
||||
() => getVariablesPosition(position, width, height),
|
||||
[position, width, height]
|
||||
@@ -102,33 +124,31 @@ export function Variables() {
|
||||
onDimensionsChange: setDimensions,
|
||||
})
|
||||
|
||||
// Data for current workflow
|
||||
const workflowVariables = useMemo(
|
||||
() => (activeWorkflowId ? getVariablesByWorkflowId(activeWorkflowId) : []),
|
||||
[activeWorkflowId, getVariablesByWorkflowId, variables]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeWorkflowId) {
|
||||
loadForWorkflow(activeWorkflowId).catch((e) => logger.error('loadForWorkflow failed', e))
|
||||
}
|
||||
}, [activeWorkflowId, loadForWorkflow])
|
||||
|
||||
// Ensure variables are loaded when the modal is opened
|
||||
useEffect(() => {
|
||||
if (isOpen && activeWorkflowId) {
|
||||
loadForWorkflow(activeWorkflowId).catch((e) => logger.error('loadForWorkflow failed', e))
|
||||
}
|
||||
}, [isOpen, activeWorkflowId, loadForWorkflow])
|
||||
|
||||
// Local per-variable UI state
|
||||
const [collapsedById, setCollapsedById] = useState<Record<string, boolean>>({})
|
||||
const [localNames, setLocalNames] = useState<Record<string, string>>({})
|
||||
const [nameErrors, setNameErrors] = useState<Record<string, string>>({})
|
||||
const cleanupState = useCallback(
|
||||
(
|
||||
setter: React.Dispatch<React.SetStateAction<Record<string, any>>>,
|
||||
currentIds: Set<string>
|
||||
) => {
|
||||
setter((prev) => {
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(prev).filter(([id]) => currentIds.has(id))
|
||||
)
|
||||
return Object.keys(filtered).length !== Object.keys(prev).length ? filtered : prev
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const currentVariableIds = new Set(workflowVariables.map((v) => v.id))
|
||||
cleanupState(setCollapsedById, currentVariableIds)
|
||||
cleanupState(setLocalNames, currentVariableIds)
|
||||
cleanupState(setNameErrors, currentVariableIds)
|
||||
}, [workflowVariables, cleanupState])
|
||||
|
||||
/**
|
||||
* Toggles the collapsed state of a variable
|
||||
*/
|
||||
const toggleCollapsed = (variableId: string) => {
|
||||
setCollapsedById((prev) => ({
|
||||
...prev,
|
||||
@@ -136,26 +156,21 @@ export function Variables() {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear local name/error state for a variable
|
||||
*/
|
||||
const clearLocalState = (variableId: string) => {
|
||||
setLocalNames((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[variableId]
|
||||
return updated
|
||||
})
|
||||
setNameErrors((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[variableId]
|
||||
return updated
|
||||
})
|
||||
const handleHeaderKeyDown = (e: React.KeyboardEvent, variableId: string) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
toggleCollapsed(variableId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear error for a specific variable if present
|
||||
*/
|
||||
const clearError = (variableId: string) => {
|
||||
const clearVariableState = (variableId: string, clearNames = true) => {
|
||||
if (clearNames) {
|
||||
setLocalNames((prev) => {
|
||||
const updated = { ...prev }
|
||||
delete updated[variableId]
|
||||
return updated
|
||||
})
|
||||
}
|
||||
setNameErrors((prev) => {
|
||||
if (!prev[variableId]) return prev
|
||||
const updated = { ...prev }
|
||||
@@ -164,185 +179,195 @@ export function Variables() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new variable to the list
|
||||
*/
|
||||
const handleAddVariable = () => {
|
||||
const handleAddVariable = useCallback(() => {
|
||||
if (!activeWorkflowId) return
|
||||
addVariable({
|
||||
collaborativeAddVariable({
|
||||
name: '',
|
||||
type: 'plain',
|
||||
value: '',
|
||||
workflowId: activeWorkflowId,
|
||||
})
|
||||
}
|
||||
}, [activeWorkflowId, collaborativeAddVariable])
|
||||
|
||||
/**
|
||||
* Removes a variable by ID
|
||||
*/
|
||||
const handleRemoveVariable = (variableId: string) => {
|
||||
deleteVariable(variableId)
|
||||
}
|
||||
const handleRemoveVariable = useCallback(
|
||||
(variableId: string) => {
|
||||
collaborativeDeleteVariable(variableId)
|
||||
},
|
||||
[collaborativeDeleteVariable]
|
||||
)
|
||||
|
||||
/**
|
||||
* Updates a specific variable property
|
||||
*/
|
||||
const handleUpdateVariable = (variableId: string, field: keyof Variable, value: any) => {
|
||||
const validatedValue =
|
||||
field === 'name' && typeof value === 'string' ? validateName(value) : value
|
||||
updateVariable(variableId, { [field]: validatedValue })
|
||||
}
|
||||
const handleUpdateVariable = useCallback(
|
||||
(variableId: string, field: 'name' | 'value' | 'type', value: any) => {
|
||||
collaborativeUpdateVariable(variableId, field, value)
|
||||
},
|
||||
[collaborativeUpdateVariable]
|
||||
)
|
||||
const isDuplicateName = useCallback(
|
||||
(variableId: string, name: string): boolean => {
|
||||
const trimmedName = name.trim()
|
||||
return (
|
||||
!!trimmedName &&
|
||||
workflowVariables.some((v) => v.id !== variableId && v.name === trimmedName)
|
||||
)
|
||||
},
|
||||
[workflowVariables]
|
||||
)
|
||||
|
||||
/**
|
||||
* Local handlers for name editing with validation akin to panel behavior
|
||||
*/
|
||||
const isDuplicateName = (variableId: string, name: string): boolean => {
|
||||
if (!name.trim()) return false
|
||||
return workflowVariables.some((v) => v.id !== variableId && v.name === name.trim())
|
||||
}
|
||||
|
||||
const handleVariableNameChange = (variableId: string, newName: string) => {
|
||||
const handleVariableNameChange = useCallback((variableId: string, newName: string) => {
|
||||
const validatedName = validateName(newName)
|
||||
setLocalNames((prev) => ({
|
||||
...prev,
|
||||
[variableId]: validatedName,
|
||||
}))
|
||||
clearError(variableId)
|
||||
}
|
||||
clearVariableState(variableId, false)
|
||||
}, [])
|
||||
|
||||
const handleVariableNameBlur = (variableId: string) => {
|
||||
const localName = localNames[variableId]
|
||||
if (localName === undefined) return
|
||||
const handleVariableNameBlur = useCallback(
|
||||
(variableId: string) => {
|
||||
const localName = localNames[variableId]
|
||||
if (localName === undefined) return
|
||||
|
||||
const trimmedName = localName.trim()
|
||||
if (!trimmedName) {
|
||||
setNameErrors((prev) => ({
|
||||
...prev,
|
||||
[variableId]: 'Variable name cannot be empty',
|
||||
}))
|
||||
return
|
||||
}
|
||||
const trimmedName = localName.trim()
|
||||
if (!trimmedName) {
|
||||
setNameErrors((prev) => ({
|
||||
...prev,
|
||||
[variableId]: STRINGS.errors.emptyName,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
if (isDuplicateName(variableId, trimmedName)) {
|
||||
setNameErrors((prev) => ({
|
||||
...prev,
|
||||
[variableId]: 'Two variables cannot have the same name',
|
||||
}))
|
||||
return
|
||||
}
|
||||
if (isDuplicateName(variableId, trimmedName)) {
|
||||
setNameErrors((prev) => ({
|
||||
...prev,
|
||||
[variableId]: STRINGS.errors.duplicateName,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
updateVariable(variableId, { name: trimmedName })
|
||||
clearLocalState(variableId)
|
||||
}
|
||||
collaborativeUpdateVariable(variableId, 'name', trimmedName)
|
||||
clearVariableState(variableId)
|
||||
},
|
||||
[localNames, isDuplicateName, collaborativeUpdateVariable]
|
||||
)
|
||||
|
||||
const handleVariableNameKeyDown = (
|
||||
variableId: string,
|
||||
e: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
const handleVariableNameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats variable value for display in editor
|
||||
*/
|
||||
const formatValue = (variable: Variable) => {
|
||||
if (variable.value === '') return ''
|
||||
return typeof variable.value === 'string' ? variable.value : JSON.stringify(variable.value)
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [setIsOpen])
|
||||
|
||||
/**
|
||||
* Renders the variable header with name, type badge, and action buttons
|
||||
*/
|
||||
const renderVariableHeader = (variable: Variable, index: number) => (
|
||||
<div
|
||||
className='flex cursor-pointer items-center justify-between bg-transparent px-[10px] py-[5px]'
|
||||
onClick={() => toggleCollapsed(variable.id)}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[#AEAEAE] text-[14px]'>
|
||||
{variable.name || `Variable ${index + 1}`}
|
||||
</span>
|
||||
{variable.name && <Badge className='h-[20px] text-[13px]'>{variable.type}</Badge>}
|
||||
</div>
|
||||
<div className='flex items-center gap-[8px] pl-[8px]' onClick={(e) => e.stopPropagation()}>
|
||||
<Button variant='ghost' onClick={handleAddVariable} className='h-auto p-0'>
|
||||
<Plus className='h-[14px] w-[14px]' />
|
||||
<span className='sr-only'>Add Variable</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleRemoveVariable(variable.id)}
|
||||
className='h-auto p-0 text-[#EF4444] hover:text-[#EF4444]'
|
||||
const renderVariableHeader = useCallback(
|
||||
(variable: Variable, index: number) => {
|
||||
const isCollapsed = collapsedById[variable.id] ?? false
|
||||
return (
|
||||
<div
|
||||
className='flex cursor-pointer items-center justify-between bg-transparent px-[10px] py-[5px]'
|
||||
onClick={() => toggleCollapsed(variable.id)}
|
||||
onKeyDown={(e) => handleHeaderKeyDown(e, variable.id)}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-expanded={!isCollapsed}
|
||||
aria-controls={`variable-content-${variable.id}`}
|
||||
>
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
<span className='sr-only'>Delete Variable</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||
<span className='block truncate font-medium text-[#AEAEAE] text-[14px]'>
|
||||
{variable.name || `Variable ${index + 1}`}
|
||||
</span>
|
||||
{variable.name && (
|
||||
<Badge style={{ height: `${BADGE_HEIGHT}px`, fontSize: `${BADGE_TEXT_SIZE}px` }}>
|
||||
{variable.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemoveVariable(variable.id)
|
||||
}}
|
||||
className='h-auto p-0 text-[#EF4444] hover:text-[#EF4444]'
|
||||
aria-label={`Delete ${variable.name || `variable ${index + 1}`}`}
|
||||
>
|
||||
<Trash style={{ width: `${ICON_SIZE}px`, height: `${ICON_SIZE}px` }} />
|
||||
<span className='sr-only'>Delete Variable</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[collapsedById, toggleCollapsed, handleRemoveVariable]
|
||||
)
|
||||
|
||||
/**
|
||||
* Renders the value input based on variable type
|
||||
* Renders the value input based on variable type.
|
||||
* Memoized with useCallback to prevent unnecessary re-renders.
|
||||
*/
|
||||
const renderValueInput = (variable: Variable) => {
|
||||
const variableValue = formatValue(variable)
|
||||
const renderValueInput = useCallback(
|
||||
(variable: Variable) => {
|
||||
const variableValue =
|
||||
variable.value === ''
|
||||
? ''
|
||||
: typeof variable.value === 'string'
|
||||
? variable.value
|
||||
: JSON.stringify(variable.value)
|
||||
|
||||
if (variable.type === 'object' || variable.type === 'array') {
|
||||
const lineCount = variableValue.split('\n').length
|
||||
const gutterWidth = calculateGutterWidth(lineCount)
|
||||
const placeholder = variable.type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
|
||||
if (variable.type === 'object' || variable.type === 'array') {
|
||||
const lineCount = variableValue.split('\n').length
|
||||
const gutterWidth = calculateGutterWidth(lineCount)
|
||||
const placeholder =
|
||||
variable.type === 'object' ? STRINGS.placeholders.object : STRINGS.placeholders.array
|
||||
|
||||
const renderLineNumbers = () => {
|
||||
return Array.from({ length: lineCount }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='font-medium font-mono text-[#787878] text-xs'
|
||||
style={{ height: `${21}px`, lineHeight: `${21}px` }}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
))
|
||||
const renderLineNumbers = () => {
|
||||
return Array.from({ length: lineCount }, (_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className='font-medium font-mono text-[#787878] text-xs'
|
||||
style={{ height: `${LINE_HEIGHT}px`, lineHeight: `${LINE_HEIGHT}px` }}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<Code.Container style={{ minHeight: `${MIN_EDITOR_HEIGHT}px` }}>
|
||||
<Code.Gutter width={gutterWidth}>{renderLineNumbers()}</Code.Gutter>
|
||||
<Code.Content paddingLeft={`${gutterWidth}px`}>
|
||||
<Code.Placeholder gutterWidth={gutterWidth} show={variableValue.length === 0}>
|
||||
{placeholder}
|
||||
</Code.Placeholder>
|
||||
<Editor
|
||||
value={variableValue}
|
||||
onValueChange={(newValue) => handleUpdateVariable(variable.id, 'value', newValue)}
|
||||
highlight={(code) => highlight(code, languages.json, 'json')}
|
||||
{...getCodeEditorProps()}
|
||||
/>
|
||||
</Code.Content>
|
||||
</Code.Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Code.Container className='min-h-[120px]'>
|
||||
<Code.Gutter width={gutterWidth}>{renderLineNumbers()}</Code.Gutter>
|
||||
<Code.Content paddingLeft={`${gutterWidth}px`}>
|
||||
<Code.Placeholder gutterWidth={gutterWidth} show={variableValue.length === 0}>
|
||||
{placeholder}
|
||||
</Code.Placeholder>
|
||||
<Editor
|
||||
value={variableValue}
|
||||
onValueChange={(newValue) => handleUpdateVariable(variable.id, 'value', newValue)}
|
||||
highlight={(code) => highlight(code, languages.json, 'json')}
|
||||
{...getCodeEditorProps()}
|
||||
/>
|
||||
</Code.Content>
|
||||
</Code.Container>
|
||||
<Input
|
||||
name='value'
|
||||
autoComplete='off'
|
||||
value={variableValue}
|
||||
onChange={(e) => handleUpdateVariable(variable.id, 'value', e.target.value)}
|
||||
placeholder={
|
||||
variable.type === 'number'
|
||||
? STRINGS.placeholders.number
|
||||
: variable.type === 'boolean'
|
||||
? STRINGS.placeholders.boolean
|
||||
: STRINGS.placeholders.plain
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
name='value'
|
||||
value={variableValue}
|
||||
onChange={(e) => handleUpdateVariable(variable.id, 'value', e.target.value)}
|
||||
placeholder={
|
||||
variable.type === 'number'
|
||||
? '42'
|
||||
: variable.type === 'boolean'
|
||||
? 'true'
|
||||
: 'Plain text value'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
[handleUpdateVariable]
|
||||
)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
@@ -376,11 +401,17 @@ export function Variables() {
|
||||
e.stopPropagation()
|
||||
handleAddVariable()
|
||||
}}
|
||||
aria-label='Add new variable'
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px]' />
|
||||
<Plus style={{ width: `${HEADER_ICON_SIZE}px`, height: `${HEADER_ICON_SIZE}px` }} />
|
||||
</Button>
|
||||
<Button variant='ghost' className='!p-1.5 -m-1.5' onClick={handleClose}>
|
||||
<X className='h-[16px] w-[16px]' />
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='!p-1.5 -m-1.5'
|
||||
onClick={handleClose}
|
||||
aria-label='Close variables panel'
|
||||
>
|
||||
<X style={{ width: `${HEADER_ICON_SIZE}px`, height: `${HEADER_ICON_SIZE}px` }} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -389,7 +420,7 @@ export function Variables() {
|
||||
<div className='flex flex-1 flex-col overflow-hidden pt-[8px]'>
|
||||
{workflowVariables.length === 0 ? (
|
||||
<div className='flex flex-1 items-center justify-center text-[#8D8D8D] text-[13px]'>
|
||||
No variables yet
|
||||
{STRINGS.emptyState}
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-full overflow-y-auto overflow-x-hidden'>
|
||||
@@ -405,24 +436,30 @@ export function Variables() {
|
||||
{renderVariableHeader(variable, index)}
|
||||
|
||||
{!(collapsedById[variable.id] ?? false) && (
|
||||
<div className='flex flex-col gap-[6px] border-[#303030] border-t px-[10px] pt-[6px] pb-[10px]'>
|
||||
<div
|
||||
id={`variable-content-${variable.id}`}
|
||||
className='flex flex-col gap-[6px] border-[#303030] border-t px-[10px] pt-[6px] pb-[10px]'
|
||||
>
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label className='text-[13px]'>Name</Label>
|
||||
<Label className='text-[13px]'>{STRINGS.labels.name}</Label>
|
||||
<Input
|
||||
name='name'
|
||||
autoComplete='off'
|
||||
value={localNames[variable.id] ?? variable.name}
|
||||
onChange={(e) => handleVariableNameChange(variable.id, e.target.value)}
|
||||
onBlur={() => handleVariableNameBlur(variable.id)}
|
||||
onKeyDown={(e) => handleVariableNameKeyDown(variable.id, e)}
|
||||
placeholder='variableName'
|
||||
onKeyDown={handleVariableNameKeyDown}
|
||||
placeholder={STRINGS.placeholders.name}
|
||||
/>
|
||||
{nameErrors[variable.id] && (
|
||||
<div className='mt-1 text-red-400 text-xs'>{nameErrors[variable.id]}</div>
|
||||
<p className='text-[#EF4444] text-xs' role='alert'>
|
||||
{nameErrors[variable.id]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-[4px]'>
|
||||
<Label className='text-[13px]'>Type</Label>
|
||||
<Label className='text-[13px]'>{STRINGS.labels.type}</Label>
|
||||
<Combobox
|
||||
options={TYPE_OPTIONS}
|
||||
value={variable.type}
|
||||
@@ -431,7 +468,7 @@ export function Variables() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-[4px]'>
|
||||
<Label className='text-[13px]'>Value</Label>
|
||||
<Label className='text-[13px]'>{STRINGS.labels.value}</Label>
|
||||
<div className='relative'>{renderValueInput(variable)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -380,6 +380,15 @@ export function useCollaborativeWorkflow() {
|
||||
payload.id
|
||||
)
|
||||
break
|
||||
case 'variable-update':
|
||||
if (payload.field === 'name') {
|
||||
variablesStore.updateVariable(payload.variableId, { name: payload.value })
|
||||
} else if (payload.field === 'value') {
|
||||
variablesStore.updateVariable(payload.variableId, { value: payload.value })
|
||||
} else if (payload.field === 'type') {
|
||||
variablesStore.updateVariable(payload.variableId, { type: payload.value })
|
||||
}
|
||||
break
|
||||
case 'remove':
|
||||
variablesStore.deleteVariable(payload.variableId)
|
||||
break
|
||||
@@ -1527,6 +1536,8 @@ export function useCollaborativeWorkflow() {
|
||||
const collaborativeAddVariable = useCallback(
|
||||
(variableData: { name: string; type: any; value: any; workflowId: string }) => {
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// Optimistically add to local store first
|
||||
variablesStore.addVariable(variableData, id)
|
||||
const processedVariable = useVariablesStore.getState().variables[id]
|
||||
|
||||
@@ -1537,6 +1548,8 @@ export function useCollaborativeWorkflow() {
|
||||
name: processedVariable.name,
|
||||
}
|
||||
|
||||
// Queue operation with processed name for server & other clients
|
||||
// Empty callback because local store is already updated above
|
||||
executeQueuedOperation('add', 'variable', payloadWithProcessedName, () => {})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user