From 5f05d6beb1422c4d2d40b3510afd9c5bf5b4730f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 30 Jul 2026 20:20:55 -0700 Subject: [PATCH 1/2] improvement(function): secrets access dropdown --- .../components/dropdown/dropdown.tsx | 27 ++++++++++++++----- .../editor/components/sub-block/sub-block.tsx | 1 + apps/sim/blocks/blocks/function.ts | 3 +++ apps/sim/blocks/types.ts | 9 +++++++ apps/sim/lib/workflows/subblocks/options.ts | 7 +++-- .../emcn/src/components/chip-tag/chip-tag.tsx | 8 +++++- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index cf50fe16738..1eb22d6b5a8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -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 } /** @@ -92,6 +94,7 @@ export const Dropdown = memo(function Dropdown({ fetchOptionById, dependsOn, searchable = false, + preserveLabelCase = false, }: DropdownProps) { const activeSearchTarget = useActiveSearchTarget() const { isToolAllowed } = usePermissionConfig() @@ -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])) @@ -370,7 +374,8 @@ export const Dropdown = memo(function Dropdown({ return (
{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, @@ -379,7 +384,7 @@ export const Dropdown = memo(function Dropdown({ label, }) return ( - + {formatDisplayText(label, { workflowSearchHighlight })} @@ -387,13 +392,21 @@ export const Dropdown = memo(function Dropdown({ ) })} {overflowCount > 0 && ( - + +{overflowCount} )}
) - }, [activeSearchTarget, blockId, multiSelect, multiValues, optionMap, subBlockId]) + }, [ + activeSearchTarget, + blockId, + multiSelect, + multiValues, + optionMap, + preserveLabelCase, + subBlockId, + ]) const singleSelectOverlay = useMemo(() => { if (multiSelect || !singleValue) return undefined diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index a860fcfcc33..8832dd0ef54 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -689,6 +689,7 @@ function SubBlockComponent({ fetchOptionById={config.fetchOptionById} dependsOn={config.dependsOn} searchable={config.searchable} + preserveLabelCase={config.preserveLabelCase} /> ) diff --git a/apps/sim/blocks/blocks/function.ts b/apps/sim/blocks/blocks/function.ts index 6229b718a75..ea8acae1fcb 100644 --- a/apps/sim/blocks/blocks/function.ts +++ b/apps/sim/blocks/blocks/function.ts @@ -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', diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 2b29a9a1ab4..716625d7102 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -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. */ diff --git a/apps/sim/lib/workflows/subblocks/options.ts b/apps/sim/lib/workflows/subblocks/options.ts index 9c6dc78216a..a4d721d1c35 100644 --- a/apps/sim/lib/workflows/subblocks/options.ts +++ b/apps/sim/lib/workflows/subblocks/options.ts @@ -63,9 +63,12 @@ export async function fetchWorkspaceSecretNameOptions(): Promisevalue record (see `workspaceEnvironmentDataSchema`); reaching for + // a `.variables` sub-key under it yields undefined and silently drops every + // workspace secret from the picker. const names = new Set([ - ...Object.keys(workspace?.workspace?.variables ?? {}), + ...Object.keys(workspace?.workspace ?? {}), ...Object.keys(personal ?? {}), ]) return [...names].sort().map((name) => ({ id: name, label: name })) diff --git a/packages/emcn/src/components/chip-tag/chip-tag.tsx b/packages/emcn/src/components/chip-tag/chip-tag.tsx index 32fe4cf022f..e69ae59c429 100644 --- a/packages/emcn/src/components/chip-tag/chip-tag.tsx +++ b/packages/emcn/src/components/chip-tag/chip-tag.tsx @@ -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`) @@ -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: From f0959d85f7709566cdf7f047728f1cac2f1b2cc0 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 30 Jul 2026 20:29:13 -0700 Subject: [PATCH 2/2] fix --- apps/sim/lib/workflows/subblocks/options.ts | 48 ++++++++++----------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/apps/sim/lib/workflows/subblocks/options.ts b/apps/sim/lib/workflows/subblocks/options.ts index a4d721d1c35..8f5baab9b33 100644 --- a/apps/sim/lib/workflows/subblocks/options.ts +++ b/apps/sim/lib/workflows/subblocks/options.ts @@ -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' @@ -43,33 +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 { 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.workspace` is already the - // flat name->value record (see `workspaceEnvironmentDataSchema`); reaching for - // a `.variables` sub-key under it yields undefined and silently drops every - // workspace secret from the picker. + // 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([ - ...Object.keys(workspace?.workspace ?? {}), - ...Object.keys(personal ?? {}), + ...Object.keys(environment?.workspace ?? {}), + ...Object.keys(environment?.personal ?? {}), ]) return [...names].sort().map((name) => ({ id: name, label: name })) }