mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(function): secrets access dropdown (#6118)
This commit is contained in:
+20
-7
@@ -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 (
|
||||
<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,
|
||||
@@ -379,7 +384,7 @@ 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>
|
||||
@@ -387,13 +392,21 @@ export const Dropdown = memo(function Dropdown({
|
||||
)
|
||||
})}
|
||||
{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
|
||||
|
||||
+1
@@ -689,6 +689,7 @@ function SubBlockComponent({
|
||||
fetchOptionById={config.fetchOptionById}
|
||||
dependsOn={config.dependsOn}
|
||||
searchable={config.searchable}
|
||||
preserveLabelCase={config.preserveLabelCase}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,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 }))
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user