Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ interface DropdownProps {
dependsOn?: SubBlockConfig['dependsOn']
/** Enable search input in dropdown */
searchable?: boolean
/** Render option labels verbatim instead of lowercasing them */
preserveLabelCase?: boolean
}

/**
Expand All @@ -92,6 +94,7 @@ export const Dropdown = memo(function Dropdown({
fetchOptionById,
dependsOn,
searchable = false,
preserveLabelCase = false,
}: DropdownProps) {
const activeSearchTarget = useActiveSearchTarget()
const { isToolAllowed } = usePermissionConfig()
Expand Down Expand Up @@ -208,18 +211,19 @@ export const Dropdown = memo(function Dropdown({
}, [subBlockId, blockConfig, allOptions, isToolAllowed])

const comboboxOptions = useMemo((): ComboboxOption[] => {
const toLabel = (raw: string) => (preserveLabelCase ? raw : raw.toLowerCase())
return allOptions.map((opt) => {
if (typeof opt === 'string') {
return { label: opt.toLowerCase(), value: opt, hidden: deniedOperationIds.has(opt) }
return { label: toLabel(opt), value: opt, hidden: deniedOperationIds.has(opt) }
}
return {
label: opt.label.toLowerCase(),
label: toLabel(opt.label),
value: opt.id,
icon: 'icon' in opt ? opt.icon : undefined,
hidden: opt.hidden || deniedOperationIds.has(opt.id),
}
})
}, [allOptions, deniedOperationIds])
}, [allOptions, deniedOperationIds, preserveLabelCase])

const optionMap = useMemo(() => {
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))
Expand Down Expand Up @@ -370,7 +374,8 @@ export const Dropdown = memo(function Dropdown({
return (
<div className='flex items-center gap-1 overflow-hidden whitespace-nowrap'>
{visibleValues.map((selectedValue: string, index) => {
const label = (optionMap.get(selectedValue) || selectedValue).toLowerCase()
const rawLabel = optionMap.get(selectedValue) || selectedValue
const label = preserveLabelCase ? rawLabel : rawLabel.toLowerCase()
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
activeSearchTarget,
blockId,
Expand All @@ -379,21 +384,29 @@ export const Dropdown = memo(function Dropdown({
label,
})
return (
<ChipTag key={selectedValue} variant='mono' className='min-w-0 shrink'>
<ChipTag key={selectedValue} variant='field' className='min-w-0 shrink'>
<span className='truncate'>
{formatDisplayText(label, { workflowSearchHighlight })}
</span>
</ChipTag>
)
})}
{overflowCount > 0 && (
<ChipTag variant='mono' className='shrink-0'>
<ChipTag variant='field' className='shrink-0'>
+{overflowCount}
</ChipTag>
)}
</div>
)
}, [activeSearchTarget, blockId, multiSelect, multiValues, optionMap, subBlockId])
}, [
activeSearchTarget,
blockId,
multiSelect,
multiValues,
optionMap,
preserveLabelCase,
subBlockId,
])

const singleSelectOverlay = useMemo(() => {
if (multiSelect || !singleValue) return undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,7 @@ function SubBlockComponent({
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
searchable={config.searchable}
preserveLabelCase={config.preserveLabelCase}
/>
</div>
)
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/blocks/blocks/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ try {
paramVisibility: 'user-only',
multiSelect: true,
searchable: true,
// Secret names are case-sensitive: the code references the exact name via
// `{{NAME}}`, so the picker must not lowercase what it shows.
preserveLabelCase: true,
options: [],
condition: { field: 'secretScope', value: 'selected' },
placeholder: 'Select secrets this tool can read',
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,15 @@ export interface SubBlockConfig {
rows?: number
// Multi-select functionality
multiSelect?: boolean
/**
* Dropdown-specific: render option labels verbatim instead of lowercasing them.
*
* The editor lowercases dropdown labels as a typographic convention, which
* suits authored operation names ("Send Message"). It corrupts labels that are
* case-sensitive identifiers the user must reproduce elsewhere — a workspace
* secret shown as `stripe_key` cannot be referenced as `{{stripe_key}}`.
*/
preserveLabelCase?: boolean
// Combobox specific: Enable search input in dropdown
searchable?: boolean
/** Dropdown-specific: include static options as Cmd K search entries that preset this subblock. */
Expand Down
45 changes: 22 additions & 23 deletions apps/sim/lib/workflows/subblocks/options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api'
import { fetchWorkspaceEnvironment } from '@/lib/environment/api'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import {
environmentKeys,
PERSONAL_ENVIRONMENT_STALE_TIME,
WORKSPACE_ENVIRONMENT_STALE_TIME,
} from '@/hooks/queries/environment'
import { environmentKeys, WORKSPACE_ENVIRONMENT_STALE_TIME } from '@/hooks/queries/environment'
import { getSandboxListQueryOptions, type SandboxListResponse } from '@/hooks/queries/sandboxes'
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
Expand Down Expand Up @@ -43,30 +39,33 @@ export async function fetchWorkspaceWorkflowOptions(options?: {
* Loads the active workspace's secret NAMES for the Function block's secret-scope
* picker. Names only — values stay server-side and are injected at execution, the
* same discipline the copilot's workspace context uses.
*
* Both halves come from the single workspace-environment response, which is the
* client-side mirror of `getEffectiveDecryptedEnv` — the resolver the executor
* injects from. Its `personal` slice is NOT the caller's raw personal variables:
* under credential filtering it also carries personal secrets other members have
* shared into the workspace, so reading `/api/environment` instead would hide
* names the code can genuinely resolve. This is the same pair the `{{VAR}}`
* autocomplete lists, so the picker and the editor never disagree.
*
* Values are masked to `''` for non-admin viewers; only the keys are used here.
*/
export async function fetchWorkspaceSecretNameOptions(): Promise<SubBlockOption[]> {
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
if (!workspaceId) return []

const [workspace, personal] = await Promise.all([
getQueryClient().fetchQuery({
queryKey: environmentKeys.workspace(workspaceId),
queryFn: ({ signal }: { signal?: AbortSignal }) =>
fetchWorkspaceEnvironment(workspaceId, signal),
staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
}),
getQueryClient().fetchQuery({
queryKey: environmentKeys.personal(),
queryFn: ({ signal }: { signal?: AbortSignal }) => fetchPersonalEnvironment(signal),
staleTime: PERSONAL_ENVIRONMENT_STALE_TIME,
}),
])
const environment = await getQueryClient().fetchQuery({
queryKey: environmentKeys.workspace(workspaceId),
queryFn: ({ signal }: { signal?: AbortSignal }) =>
fetchWorkspaceEnvironment(workspaceId, signal),
staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
})

// Personal variables shadow workspace ones at execution, so both are offered
// under one de-duplicated list of names.
// Workspace entries shadow personal ones at execution (`getEffectiveDecryptedEnv`
// spreads workspace last), but a name-only list just de-duplicates the union.
const names = new Set<string>([
...Object.keys(workspace?.workspace?.variables ?? {}),
...Object.keys(personal ?? {}),
...Object.keys(environment?.workspace ?? {}),
...Object.keys(environment?.personal ?? {}),
])
return [...names].sort().map((name) => ({ id: name, label: name }))
}
Expand Down
8 changes: 7 additions & 1 deletion packages/emcn/src/components/chip-tag/chip-tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { cn } from '../../lib/cn'
* Variants, theme-aware via workspace tokens:
* - `mono` — borderless, sharing the {@link ChipSwitch} trough surface
* (`--surface-5` light / `--surface-4` dark) with strong `--text-primary` text
* for emphasis (e.g. a discount next to a primary CTA).
* for emphasis (e.g. a discount next to a primary CTA). Because its fill *is*
* the trough/field surface, it disappears on one — use `field` there.
* - `field` — `mono` with the fill stepped one level away from the form-field
* surface (`--surface-6` light / `--surface-3` dark), so the pill stays legible
* when it sits *on* a `--surface-5` input, combobox trigger, or tag container.
* - `gray` — a light surface over a slightly darker inset ring with muted
* `--text-secondary` text for low-emphasis status labels.
* - `solid` — a filled inverse tag: a dark neutral surface (`--text-secondary`)
Expand All @@ -31,6 +35,8 @@ const chipTagVariants = cva(
variants: {
variant: {
mono: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-primary)] dark:bg-[var(--surface-4)]',
field:
'h-5 gap-[3px] px-1 bg-[var(--surface-6)] text-[var(--text-primary)] dark:bg-[var(--surface-3)]',
gray: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-secondary)] shadow-[inset_0_0_0_1px_var(--border-1)]',
solid: 'h-5 gap-[3px] px-1 bg-[var(--text-secondary)] text-[var(--text-inverse)]',
invite:
Expand Down
Loading