Skip to content

Commit 413784e

Browse files
improvement(function): secrets access dropdown (#6118)
1 parent 04a8c0a commit 413784e

6 files changed

Lines changed: 62 additions & 31 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ interface DropdownProps {
6666
dependsOn?: SubBlockConfig['dependsOn']
6767
/** Enable search input in dropdown */
6868
searchable?: boolean
69+
/** Render option labels verbatim instead of lowercasing them */
70+
preserveLabelCase?: boolean
6971
}
7072

7173
/**
@@ -92,6 +94,7 @@ export const Dropdown = memo(function Dropdown({
9294
fetchOptionById,
9395
dependsOn,
9496
searchable = false,
97+
preserveLabelCase = false,
9598
}: DropdownProps) {
9699
const activeSearchTarget = useActiveSearchTarget()
97100
const { isToolAllowed } = usePermissionConfig()
@@ -208,18 +211,19 @@ export const Dropdown = memo(function Dropdown({
208211
}, [subBlockId, blockConfig, allOptions, isToolAllowed])
209212

210213
const comboboxOptions = useMemo((): ComboboxOption[] => {
214+
const toLabel = (raw: string) => (preserveLabelCase ? raw : raw.toLowerCase())
211215
return allOptions.map((opt) => {
212216
if (typeof opt === 'string') {
213-
return { label: opt.toLowerCase(), value: opt, hidden: deniedOperationIds.has(opt) }
217+
return { label: toLabel(opt), value: opt, hidden: deniedOperationIds.has(opt) }
214218
}
215219
return {
216-
label: opt.label.toLowerCase(),
220+
label: toLabel(opt.label),
217221
value: opt.id,
218222
icon: 'icon' in opt ? opt.icon : undefined,
219223
hidden: opt.hidden || deniedOperationIds.has(opt.id),
220224
}
221225
})
222-
}, [allOptions, deniedOperationIds])
226+
}, [allOptions, deniedOperationIds, preserveLabelCase])
223227

224228
const optionMap = useMemo(() => {
225229
return new Map(comboboxOptions.map((opt) => [opt.value, opt.label]))
@@ -370,7 +374,8 @@ export const Dropdown = memo(function Dropdown({
370374
return (
371375
<div className='flex items-center gap-1 overflow-hidden whitespace-nowrap'>
372376
{visibleValues.map((selectedValue: string, index) => {
373-
const label = (optionMap.get(selectedValue) || selectedValue).toLowerCase()
377+
const rawLabel = optionMap.get(selectedValue) || selectedValue
378+
const label = preserveLabelCase ? rawLabel : rawLabel.toLowerCase()
374379
const workflowSearchHighlight = getWorkflowSearchLabelHighlight({
375380
activeSearchTarget,
376381
blockId,
@@ -379,21 +384,29 @@ export const Dropdown = memo(function Dropdown({
379384
label,
380385
})
381386
return (
382-
<ChipTag key={selectedValue} variant='mono' className='min-w-0 shrink'>
387+
<ChipTag key={selectedValue} variant='field' className='min-w-0 shrink'>
383388
<span className='truncate'>
384389
{formatDisplayText(label, { workflowSearchHighlight })}
385390
</span>
386391
</ChipTag>
387392
)
388393
})}
389394
{overflowCount > 0 && (
390-
<ChipTag variant='mono' className='shrink-0'>
395+
<ChipTag variant='field' className='shrink-0'>
391396
+{overflowCount}
392397
</ChipTag>
393398
)}
394399
</div>
395400
)
396-
}, [activeSearchTarget, blockId, multiSelect, multiValues, optionMap, subBlockId])
401+
}, [
402+
activeSearchTarget,
403+
blockId,
404+
multiSelect,
405+
multiValues,
406+
optionMap,
407+
preserveLabelCase,
408+
subBlockId,
409+
])
397410

398411
const singleSelectOverlay = useMemo(() => {
399412
if (multiSelect || !singleValue) return undefined

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ function SubBlockComponent({
689689
fetchOptionById={config.fetchOptionById}
690690
dependsOn={config.dependsOn}
691691
searchable={config.searchable}
692+
preserveLabelCase={config.preserveLabelCase}
692693
/>
693694
</div>
694695
)

apps/sim/blocks/blocks/function.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ try {
138138
paramVisibility: 'user-only',
139139
multiSelect: true,
140140
searchable: true,
141+
// Secret names are case-sensitive: the code references the exact name via
142+
// `{{NAME}}`, so the picker must not lowercase what it shows.
143+
preserveLabelCase: true,
141144
options: [],
142145
condition: { field: 'secretScope', value: 'selected' },
143146
placeholder: 'Select secrets this tool can read',

apps/sim/blocks/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,15 @@ export interface SubBlockConfig {
437437
rows?: number
438438
// Multi-select functionality
439439
multiSelect?: boolean
440+
/**
441+
* Dropdown-specific: render option labels verbatim instead of lowercasing them.
442+
*
443+
* The editor lowercases dropdown labels as a typographic convention, which
444+
* suits authored operation names ("Send Message"). It corrupts labels that are
445+
* case-sensitive identifiers the user must reproduce elsewhere — a workspace
446+
* secret shown as `stripe_key` cannot be referenced as `{{stripe_key}}`.
447+
*/
448+
preserveLabelCase?: boolean
440449
// Combobox specific: Enable search input in dropdown
441450
searchable?: boolean
442451
/** Dropdown-specific: include static options as Cmd K search entries that preset this subblock. */

apps/sim/lib/workflows/subblocks/options.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api'
1+
import { fetchWorkspaceEnvironment } from '@/lib/environment/api'
22
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
3-
import {
4-
environmentKeys,
5-
PERSONAL_ENVIRONMENT_STALE_TIME,
6-
WORKSPACE_ENVIRONMENT_STALE_TIME,
7-
} from '@/hooks/queries/environment'
3+
import { environmentKeys, WORKSPACE_ENVIRONMENT_STALE_TIME } from '@/hooks/queries/environment'
84
import { getSandboxListQueryOptions, type SandboxListResponse } from '@/hooks/queries/sandboxes'
95
import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list-query'
106
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -43,30 +39,33 @@ export async function fetchWorkspaceWorkflowOptions(options?: {
4339
* Loads the active workspace's secret NAMES for the Function block's secret-scope
4440
* picker. Names only — values stay server-side and are injected at execution, the
4541
* same discipline the copilot's workspace context uses.
42+
*
43+
* Both halves come from the single workspace-environment response, which is the
44+
* client-side mirror of `getEffectiveDecryptedEnv` — the resolver the executor
45+
* injects from. Its `personal` slice is NOT the caller's raw personal variables:
46+
* under credential filtering it also carries personal secrets other members have
47+
* shared into the workspace, so reading `/api/environment` instead would hide
48+
* names the code can genuinely resolve. This is the same pair the `{{VAR}}`
49+
* autocomplete lists, so the picker and the editor never disagree.
50+
*
51+
* Values are masked to `''` for non-admin viewers; only the keys are used here.
4652
*/
4753
export async function fetchWorkspaceSecretNameOptions(): Promise<SubBlockOption[]> {
4854
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
4955
if (!workspaceId) return []
5056

51-
const [workspace, personal] = await Promise.all([
52-
getQueryClient().fetchQuery({
53-
queryKey: environmentKeys.workspace(workspaceId),
54-
queryFn: ({ signal }: { signal?: AbortSignal }) =>
55-
fetchWorkspaceEnvironment(workspaceId, signal),
56-
staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
57-
}),
58-
getQueryClient().fetchQuery({
59-
queryKey: environmentKeys.personal(),
60-
queryFn: ({ signal }: { signal?: AbortSignal }) => fetchPersonalEnvironment(signal),
61-
staleTime: PERSONAL_ENVIRONMENT_STALE_TIME,
62-
}),
63-
])
57+
const environment = await getQueryClient().fetchQuery({
58+
queryKey: environmentKeys.workspace(workspaceId),
59+
queryFn: ({ signal }: { signal?: AbortSignal }) =>
60+
fetchWorkspaceEnvironment(workspaceId, signal),
61+
staleTime: WORKSPACE_ENVIRONMENT_STALE_TIME,
62+
})
6463

65-
// Personal variables shadow workspace ones at execution, so both are offered
66-
// under one de-duplicated list of names.
64+
// Workspace entries shadow personal ones at execution (`getEffectiveDecryptedEnv`
65+
// spreads workspace last), but a name-only list just de-duplicates the union.
6766
const names = new Set<string>([
68-
...Object.keys(workspace?.workspace?.variables ?? {}),
69-
...Object.keys(personal ?? {}),
67+
...Object.keys(environment?.workspace ?? {}),
68+
...Object.keys(environment?.personal ?? {}),
7069
])
7170
return [...names].sort().map((name) => ({ id: name, label: name }))
7271
}

packages/emcn/src/components/chip-tag/chip-tag.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import { cn } from '../../lib/cn'
1212
* Variants, theme-aware via workspace tokens:
1313
* - `mono` — borderless, sharing the {@link ChipSwitch} trough surface
1414
* (`--surface-5` light / `--surface-4` dark) with strong `--text-primary` text
15-
* for emphasis (e.g. a discount next to a primary CTA).
15+
* for emphasis (e.g. a discount next to a primary CTA). Because its fill *is*
16+
* the trough/field surface, it disappears on one — use `field` there.
17+
* - `field` — `mono` with the fill stepped one level away from the form-field
18+
* surface (`--surface-6` light / `--surface-3` dark), so the pill stays legible
19+
* when it sits *on* a `--surface-5` input, combobox trigger, or tag container.
1620
* - `gray` — a light surface over a slightly darker inset ring with muted
1721
* `--text-secondary` text for low-emphasis status labels.
1822
* - `solid` — a filled inverse tag: a dark neutral surface (`--text-secondary`)
@@ -31,6 +35,8 @@ const chipTagVariants = cva(
3135
variants: {
3236
variant: {
3337
mono: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-primary)] dark:bg-[var(--surface-4)]',
38+
field:
39+
'h-5 gap-[3px] px-1 bg-[var(--surface-6)] text-[var(--text-primary)] dark:bg-[var(--surface-3)]',
3440
gray: 'h-5 gap-[3px] px-1 bg-[var(--surface-5)] text-[var(--text-secondary)] shadow-[inset_0_0_0_1px_var(--border-1)]',
3541
solid: 'h-5 gap-[3px] px-1 bg-[var(--text-secondary)] text-[var(--text-inverse)]',
3642
invite:

0 commit comments

Comments
 (0)