chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { isHosted } from '@/lib/core/config/env-flags'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
|
||||
export type CanonicalMode = 'basic' | 'advanced'
|
||||
|
||||
export interface CanonicalGroup {
|
||||
canonicalId: string
|
||||
basicId?: string
|
||||
advancedIds: string[]
|
||||
}
|
||||
|
||||
export interface CanonicalIndex {
|
||||
groupsById: Record<string, CanonicalGroup>
|
||||
canonicalIdBySubBlockId: Record<string, string>
|
||||
}
|
||||
|
||||
export interface SubBlockCondition {
|
||||
field: string
|
||||
value: string | number | boolean | Array<string | number | boolean> | undefined
|
||||
not?: boolean
|
||||
and?: SubBlockCondition
|
||||
}
|
||||
|
||||
export interface CanonicalModeOverrides {
|
||||
[canonicalId: string]: CanonicalMode | undefined
|
||||
}
|
||||
|
||||
export interface CanonicalValueSelection {
|
||||
basicValue: unknown
|
||||
advancedValue: unknown
|
||||
advancedSourceId?: string
|
||||
}
|
||||
|
||||
interface TriggerVisibilityBlockConfig {
|
||||
category?: string
|
||||
triggers?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export function parseDependsOn(dependsOn: SubBlockConfig['dependsOn']): {
|
||||
allFields: string[]
|
||||
anyFields: string[]
|
||||
allDependsOnFields: string[]
|
||||
} {
|
||||
if (!dependsOn) {
|
||||
return { allFields: [], anyFields: [], allDependsOnFields: [] }
|
||||
}
|
||||
|
||||
if (Array.isArray(dependsOn)) {
|
||||
return { allFields: dependsOn, anyFields: [], allDependsOnFields: dependsOn }
|
||||
}
|
||||
|
||||
const allFields = dependsOn.all || []
|
||||
const anyFields = dependsOn.any || []
|
||||
return {
|
||||
allFields,
|
||||
anyFields,
|
||||
allDependsOnFields: [...allFields, ...anyFields],
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeDependencyValue(rawValue: unknown): unknown {
|
||||
if (rawValue === null || rawValue === undefined) return null
|
||||
|
||||
if (typeof rawValue === 'object') {
|
||||
if (Array.isArray(rawValue)) {
|
||||
if (rawValue.length === 0) return null
|
||||
return rawValue.map((item) => normalizeDependencyValue(item))
|
||||
}
|
||||
|
||||
const record = rawValue as Record<string, unknown>
|
||||
if ('value' in record) return normalizeDependencyValue(record.value)
|
||||
if ('id' in record) return record.id
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
return rawValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a flat map of subblock values keyed by subblock id.
|
||||
*/
|
||||
export function buildSubBlockValues(
|
||||
subBlocks: Record<string, { value?: unknown } | null | undefined>
|
||||
): Record<string, unknown> {
|
||||
return Object.entries(subBlocks).reduce<Record<string, unknown>>((acc, [key, subBlock]) => {
|
||||
acc[key] = subBlock?.value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Build canonical group indices for a block's subblocks.
|
||||
*/
|
||||
export function buildCanonicalIndex(subBlocks: SubBlockConfig[]): CanonicalIndex {
|
||||
const groupsById: Record<string, CanonicalGroup> = {}
|
||||
const canonicalIdBySubBlockId: Record<string, string> = {}
|
||||
|
||||
subBlocks.forEach((subBlock) => {
|
||||
if (!subBlock.canonicalParamId) return
|
||||
const canonicalId = subBlock.canonicalParamId
|
||||
if (!groupsById[canonicalId]) {
|
||||
groupsById[canonicalId] = { canonicalId, advancedIds: [] }
|
||||
}
|
||||
const group = groupsById[canonicalId]
|
||||
if (subBlock.mode === 'advanced' || subBlock.mode === 'trigger-advanced') {
|
||||
// Deduplicate: trigger spreads may repeat the same advanced ID as the regular block
|
||||
if (!group.advancedIds.includes(subBlock.id)) {
|
||||
group.advancedIds.push(subBlock.id)
|
||||
}
|
||||
} else {
|
||||
// A trigger-mode subblock must not overwrite a basicId already claimed by a non-trigger subblock.
|
||||
// Blocks spread their trigger's subBlocks after their own, so the regular subblock always wins.
|
||||
if (!group.basicId || subBlock.mode !== 'trigger') {
|
||||
group.basicId = subBlock.id
|
||||
}
|
||||
}
|
||||
canonicalIdBySubBlockId[subBlock.id] = canonicalId
|
||||
})
|
||||
|
||||
return { groupsById, canonicalIdBySubBlockId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve if a canonical group is a swap pair (basic + advanced).
|
||||
*/
|
||||
export function isCanonicalPair(group?: CanonicalGroup): boolean {
|
||||
return Boolean(group?.basicId && group?.advancedIds?.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds default canonical mode overrides for a block's subblocks.
|
||||
* All canonical pairs default to `'basic'`.
|
||||
*/
|
||||
export function buildDefaultCanonicalModes(
|
||||
subBlocks: SubBlockConfig[]
|
||||
): Record<string, 'basic' | 'advanced'> {
|
||||
const index = buildCanonicalIndex(subBlocks)
|
||||
const modes: Record<string, 'basic' | 'advanced'> = {}
|
||||
for (const group of Object.values(index.groupsById)) {
|
||||
if (isCanonicalPair(group)) {
|
||||
modes[group.canonicalId] = 'basic'
|
||||
}
|
||||
}
|
||||
return modes
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the active mode for a canonical group.
|
||||
*/
|
||||
export function resolveCanonicalMode(
|
||||
group: CanonicalGroup,
|
||||
values: Record<string, unknown>,
|
||||
overrides?: CanonicalModeOverrides
|
||||
): CanonicalMode {
|
||||
const override = overrides?.[group.canonicalId]
|
||||
if (override === 'advanced' && group.advancedIds.length > 0) return 'advanced'
|
||||
if (override === 'basic' && group.basicId) return 'basic'
|
||||
|
||||
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
||||
const hasBasic = isNonEmptyValue(basicValue)
|
||||
const hasAdvanced = isNonEmptyValue(advancedValue)
|
||||
|
||||
if (!group.basicId) return 'advanced'
|
||||
if (!hasBasic && hasAdvanced) return 'advanced'
|
||||
return 'basic'
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a subblock condition against a map of raw values.
|
||||
*/
|
||||
export function evaluateSubBlockCondition(
|
||||
condition:
|
||||
| SubBlockCondition
|
||||
| ((values?: Record<string, unknown>) => SubBlockCondition)
|
||||
| undefined,
|
||||
values: Record<string, unknown>
|
||||
): boolean {
|
||||
if (!condition) return true
|
||||
const actual = typeof condition === 'function' ? condition(values) : condition
|
||||
const fieldValue = values[actual.field]
|
||||
const valueMatch = Array.isArray(actual.value)
|
||||
? fieldValue != null &&
|
||||
(actual.not
|
||||
? !actual.value.includes(fieldValue as any)
|
||||
: actual.value.includes(fieldValue as any))
|
||||
: actual.not
|
||||
? fieldValue !== actual.value
|
||||
: fieldValue === actual.value
|
||||
const andMatch = !actual.and
|
||||
? true
|
||||
: (() => {
|
||||
const andFieldValue = values[actual.and!.field]
|
||||
const andValueMatch = Array.isArray(actual.and!.value)
|
||||
? andFieldValue != null &&
|
||||
(actual.and!.not
|
||||
? !actual.and!.value.includes(andFieldValue as any)
|
||||
: actual.and!.value.includes(andFieldValue as any))
|
||||
: actual.and!.not
|
||||
? andFieldValue !== actual.and!.value
|
||||
: andFieldValue === actual.and!.value
|
||||
return andValueMatch
|
||||
})()
|
||||
|
||||
return valueMatch && andMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is considered set for advanced visibility/selection.
|
||||
*/
|
||||
export function isNonEmptyValue(value: unknown): boolean {
|
||||
if (value === null || value === undefined) return false
|
||||
if (typeof value === 'string') return value.trim().length > 0
|
||||
if (Array.isArray(value)) return value.length > 0
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve basic and advanced values for a canonical group.
|
||||
*/
|
||||
export function getCanonicalValues(
|
||||
group: CanonicalGroup,
|
||||
values: Record<string, unknown>
|
||||
): CanonicalValueSelection {
|
||||
const basicValue = group.basicId ? values[group.basicId] : undefined
|
||||
let advancedValue: unknown
|
||||
let advancedSourceId: string | undefined
|
||||
|
||||
group.advancedIds.forEach((advancedId) => {
|
||||
if (advancedValue !== undefined) return
|
||||
const candidate = values[advancedId]
|
||||
if (isNonEmptyValue(candidate)) {
|
||||
advancedValue = candidate
|
||||
advancedSourceId = advancedId
|
||||
}
|
||||
})
|
||||
|
||||
return { basicValue, advancedValue, advancedSourceId }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the ACTIVE canonical member's value for a group: the basic value in basic mode, the
|
||||
* advanced value in advanced mode (per {@link resolveCanonicalMode} - honoring an explicit
|
||||
* override, then the value heuristic). Strict: returns ONLY the active member's value with no
|
||||
* cross-mode fallback, so a dormant mode's stale value can never leak. The single source of truth
|
||||
* for "what value is live for this canonical pair" - use it instead of basic-first `||` /
|
||||
* `?? 'basic'` reads or last-write-wins scans.
|
||||
*/
|
||||
export function resolveActiveCanonicalValue(
|
||||
group: CanonicalGroup,
|
||||
values: Record<string, unknown>,
|
||||
overrides?: CanonicalModeOverrides
|
||||
): unknown {
|
||||
const mode = resolveCanonicalMode(group, values, overrides)
|
||||
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
||||
return mode === 'advanced' ? advancedValue : basicValue
|
||||
}
|
||||
|
||||
/** Extract override entries matching a `${prefix}` key into a bare-`canonicalId`-keyed object. */
|
||||
function extractPrefixedModes(
|
||||
overrides: CanonicalModeOverrides,
|
||||
prefix: string
|
||||
): CanonicalModeOverrides | undefined {
|
||||
let scoped: CanonicalModeOverrides | undefined
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (key.startsWith(prefix) && value) {
|
||||
scoped = scoped ?? {}
|
||||
scoped[key.slice(prefix.length)] = value
|
||||
}
|
||||
}
|
||||
return scoped
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the `${toolIndex}:` prefix from canonical-mode override keys, returning the overrides for a
|
||||
* nested tool keyed by bare `canonicalId`. An agent block stores its nested tools' modes scoped as
|
||||
* `${toolIndex}:${canonicalId}` — keyed by the tool's position in the `tool-input` array, not its
|
||||
* `type` — so that two tool entries of the SAME type (e.g. two Table tools on one Agent block) get
|
||||
* independent canonical modes instead of colliding on a shared `${toolType}:${canonicalId}` key.
|
||||
*
|
||||
* Falls back to the legacy `${legacyToolType}:` prefix (the pre-instance-scoping format) when no
|
||||
* index-scoped key matches, so an override saved before this scoping change isn't silently dropped -
|
||||
* it keeps applying (type-shared, the old behavior) until the user re-toggles it explicitly, at which
|
||||
* point it's rewritten under the new index-scoped key.
|
||||
*
|
||||
* Returns `undefined` when there are no overrides, no `toolIndex`, and no legacy match.
|
||||
*/
|
||||
export function scopeCanonicalModesForTool(
|
||||
overrides: CanonicalModeOverrides | undefined,
|
||||
toolIndex: number | undefined,
|
||||
legacyToolType?: string
|
||||
): CanonicalModeOverrides | undefined {
|
||||
if (!overrides) return undefined
|
||||
const scoped =
|
||||
toolIndex !== undefined ? extractPrefixedModes(overrides, `${toolIndex}:`) : undefined
|
||||
if (scoped) return scoped
|
||||
return legacyToolType ? extractPrefixedModes(overrides, `${legacyToolType}:`) : undefined
|
||||
}
|
||||
|
||||
const INDEX_SCOPED_KEY = /^(\d+):(.+)$/
|
||||
|
||||
/**
|
||||
* Canonical-mode overrides are keyed by a tool's position in its `tool-input` array
|
||||
* (`${toolIndex}:${canonicalId}`), so anything that reorders or removes tools - the editor
|
||||
* (drag-reorder, remove, delete), fork/promote copy (dropping an unresolved custom-tool/MCP
|
||||
* entry) - must carry each surviving tool's overrides to its new position and DROP the
|
||||
* vacated index. Otherwise a saved basic/advanced choice can attach to whichever DIFFERENT
|
||||
* tool later lands on that old index (e.g. a newly-added tool, always appended at the end,
|
||||
* can refill a slot a removal just freed).
|
||||
*
|
||||
* Returns the full replacement `canonicalModes` object (for an atomic whole-map write - a
|
||||
* per-key merge can't drop a key, and sequential per-key writes can clobber each other when
|
||||
* two tools swap positions), or `undefined` when nothing needs to change. A legacy,
|
||||
* non-index-scoped key (the `${toolType}:` fallback format) isn't tied to any array position,
|
||||
* so it's carried over unchanged.
|
||||
*/
|
||||
export function reindexCanonicalModesByPosition(
|
||||
newIndexByOldIndex: ReadonlyMap<number, number>,
|
||||
overrides: CanonicalModeOverrides | undefined
|
||||
): Record<string, 'basic' | 'advanced'> | undefined {
|
||||
if (!overrides) return undefined
|
||||
|
||||
let changed = false
|
||||
const result: Record<string, 'basic' | 'advanced'> = {}
|
||||
for (const [key, mode] of Object.entries(overrides)) {
|
||||
if (!mode) continue
|
||||
const match = INDEX_SCOPED_KEY.exec(key)
|
||||
if (!match) {
|
||||
result[key] = mode
|
||||
continue
|
||||
}
|
||||
const newIndex = newIndexByOldIndex.get(Number(match[1]))
|
||||
if (newIndex === undefined) {
|
||||
changed = true // Tool removed (or an already-stale index) - drop the key.
|
||||
continue
|
||||
}
|
||||
if (newIndex !== Number(match[1])) changed = true
|
||||
result[`${newIndex}:${match[2]}`] = mode
|
||||
}
|
||||
return changed ? result : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link reindexCanonicalModesByPosition}, diffing `oldTools` against `newTools` by OBJECT
|
||||
* IDENTITY to derive the old-index -> new-index map. Callers must not clone the tool objects
|
||||
* they keep (only filter/splice/reorder the array itself) - a kept-but-cloned tool (e.g. a
|
||||
* `{ ...tool, someField: x }` spread) won't match its old reference and will be treated as
|
||||
* removed. Use {@link reindexCanonicalModesByPosition} directly when a caller's remap can
|
||||
* clone kept entries (fork/promote does, to rewrite a remapped id) and already knows each
|
||||
* surviving old index's new position by other means (e.g. tracking it during the same pass
|
||||
* that builds the new array, rather than post-hoc identity comparison).
|
||||
*/
|
||||
export function reindexToolCanonicalModes<T>(
|
||||
oldTools: readonly T[],
|
||||
newTools: readonly T[],
|
||||
overrides: CanonicalModeOverrides | undefined
|
||||
): Record<string, 'basic' | 'advanced'> | undefined {
|
||||
const newIndexByRef = new Map(newTools.map((tool, index) => [tool, index]))
|
||||
const newIndexByOldIndex = new Map<number, number>()
|
||||
oldTools.forEach((tool, oldIndex) => {
|
||||
const newIndex = newIndexByRef.get(tool)
|
||||
if (newIndex !== undefined) newIndexByOldIndex.set(oldIndex, newIndex)
|
||||
})
|
||||
return reindexCanonicalModesByPosition(newIndexByOldIndex, overrides)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a block has any standalone advanced-only fields (not part of canonical pairs).
|
||||
* These require the block-level advanced mode toggle to be visible.
|
||||
*/
|
||||
export function hasStandaloneAdvancedFields(
|
||||
subBlocks: SubBlockConfig[],
|
||||
canonicalIndex: CanonicalIndex
|
||||
): boolean {
|
||||
for (const subBlock of subBlocks) {
|
||||
if (!isStandaloneAdvancedMode(subBlock.mode)) continue
|
||||
if (!canonicalIndex.canonicalIdBySubBlockId[subBlock.id]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the modes that make a field advanced-only when it is not part of a
|
||||
* canonical basic/advanced pair: a standalone `advanced` field, or a standalone
|
||||
* `trigger-advanced` field (an advanced option on a trigger block). Both are
|
||||
* hidden until the block-level advanced toggle is on.
|
||||
*/
|
||||
export function isStandaloneAdvancedMode(mode: SubBlockConfig['mode']): boolean {
|
||||
return mode === 'advanced' || mode === 'trigger-advanced'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any advanced-only or canonical advanced values are present.
|
||||
*/
|
||||
export function hasAdvancedValues(
|
||||
subBlocks: SubBlockConfig[],
|
||||
values: Record<string, unknown>,
|
||||
canonicalIndex: CanonicalIndex
|
||||
): boolean {
|
||||
const checkedCanonical = new Set<string>()
|
||||
|
||||
for (const subBlock of subBlocks) {
|
||||
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
|
||||
if (canonicalId) {
|
||||
const group = canonicalIndex.groupsById[canonicalId]
|
||||
if (group && isCanonicalPair(group) && !checkedCanonical.has(canonicalId)) {
|
||||
checkedCanonical.add(canonicalId)
|
||||
const { advancedValue } = getCanonicalValues(group, values)
|
||||
if (isNonEmptyValue(advancedValue)) return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (isStandaloneAdvancedMode(subBlock.mode) && isNonEmptyValue(values[subBlock.id])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a subblock is visible based on mode and canonical swaps.
|
||||
*/
|
||||
export function isSubBlockVisibleForMode(
|
||||
subBlock: SubBlockConfig,
|
||||
displayAdvancedOptions: boolean,
|
||||
canonicalIndex: CanonicalIndex,
|
||||
values: Record<string, unknown>,
|
||||
overrides?: CanonicalModeOverrides
|
||||
): boolean {
|
||||
const canonicalId = canonicalIndex.canonicalIdBySubBlockId[subBlock.id]
|
||||
const group = canonicalId ? canonicalIndex.groupsById[canonicalId] : undefined
|
||||
|
||||
if (group && isCanonicalPair(group)) {
|
||||
const mode = resolveCanonicalMode(group, values, overrides)
|
||||
if (mode === 'advanced') return group.advancedIds.includes(subBlock.id)
|
||||
return group.basicId === subBlock.id
|
||||
}
|
||||
|
||||
if (subBlock.mode === 'basic' && displayAdvancedOptions) return false
|
||||
// Standalone advanced-only fields (`advanced` or a trigger's `trigger-advanced`)
|
||||
// hide until the block-level advanced toggle is on.
|
||||
if (isStandaloneAdvancedMode(subBlock.mode) && !displayAdvancedOptions) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function isTriggerModeSubBlock(subBlock: Pick<SubBlockConfig, 'mode'>): boolean {
|
||||
return subBlock.mode === 'trigger' || subBlock.mode === 'trigger-advanced'
|
||||
}
|
||||
|
||||
export function isTriggerConfigSubBlock(subBlock: Pick<SubBlockConfig, 'type'>): boolean {
|
||||
return String(subBlock.type) === 'trigger-config'
|
||||
}
|
||||
|
||||
export function shouldUseSubBlockForTriggerModeCanonicalIndex(
|
||||
subBlock: Pick<SubBlockConfig, 'mode' | 'type'>
|
||||
): boolean {
|
||||
return isTriggerModeSubBlock(subBlock) || isTriggerConfigSubBlock(subBlock)
|
||||
}
|
||||
|
||||
export function isPureTriggerBlockConfig(blockConfig?: TriggerVisibilityBlockConfig): boolean {
|
||||
return Boolean(blockConfig?.triggers?.enabled && blockConfig.category === 'triggers')
|
||||
}
|
||||
|
||||
export function isSubBlockVisibleForTriggerMode(
|
||||
subBlock: Pick<SubBlockConfig, 'mode' | 'type'>,
|
||||
displayTriggerMode: boolean,
|
||||
blockConfig?: TriggerVisibilityBlockConfig
|
||||
): boolean {
|
||||
if (isTriggerConfigSubBlock(subBlock)) {
|
||||
return displayTriggerMode || isPureTriggerBlockConfig(blockConfig)
|
||||
}
|
||||
|
||||
if (isTriggerModeSubBlock(subBlock)) return displayTriggerMode
|
||||
if (displayTriggerMode) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the dependency value for a dependsOn key, honoring canonical swaps.
|
||||
*/
|
||||
export function resolveDependencyValue(
|
||||
dependencyKey: string,
|
||||
values: Record<string, unknown>,
|
||||
canonicalIndex: CanonicalIndex,
|
||||
overrides?: CanonicalModeOverrides
|
||||
): unknown {
|
||||
const canonicalId =
|
||||
canonicalIndex.groupsById[dependencyKey]?.canonicalId ||
|
||||
canonicalIndex.canonicalIdBySubBlockId[dependencyKey]
|
||||
|
||||
if (!canonicalId) {
|
||||
return values[dependencyKey]
|
||||
}
|
||||
|
||||
const group = canonicalIndex.groupsById[canonicalId]
|
||||
if (!group) return values[dependencyKey]
|
||||
|
||||
const { basicValue, advancedValue } = getCanonicalValues(group, values)
|
||||
const mode = resolveCanonicalMode(group, values, overrides)
|
||||
const canonicalResult =
|
||||
mode === 'advanced' ? (advancedValue ?? basicValue) : (basicValue ?? advancedValue)
|
||||
|
||||
if (canonicalResult != null) return canonicalResult
|
||||
|
||||
for (const [memberId, memberCanonicalId] of Object.entries(
|
||||
canonicalIndex.canonicalIdBySubBlockId
|
||||
)) {
|
||||
if (memberCanonicalId === canonicalId && isNonEmptyValue(values[memberId])) {
|
||||
return values[memberId]
|
||||
}
|
||||
}
|
||||
|
||||
return values[dependencyKey]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a subblock is gated by a feature flag.
|
||||
*/
|
||||
export function isSubBlockFeatureEnabled(subBlock: SubBlockConfig): boolean {
|
||||
if (!subBlock.showWhenEnvSet) return true
|
||||
return isTruthy(getEnv(subBlock.showWhenEnvSet))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a subblock should be hidden based on environment conditions.
|
||||
* Covers two cases:
|
||||
* - `hideWhenHosted`: hidden when running on hosted Sim (tool API key fields)
|
||||
* - `hideWhenEnvSet`: hidden when a specific NEXT_PUBLIC_ env var is truthy
|
||||
* (credential fields hidden when the deployment provides them server-side)
|
||||
*/
|
||||
export function isSubBlockHidden(subBlock: SubBlockConfig): boolean {
|
||||
if (subBlock.hideWhenHosted && isHosted) return true
|
||||
if (subBlock.hideWhenEnvSet && isTruthy(getEnv(subBlock.hideWhenEnvSet))) return true
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user