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
15 changes: 15 additions & 0 deletions .changeset/resolve-action-param-visible.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@object-ui/app-shell": patch
---

fix(app-shell): propagate action-param `visible` predicate through resolveActionParams

The create-user phone fix (#2406) gated the `phoneNumber` param with
`visible: 'features.phoneNumber == true'`, but `resolveActionParam` dropped
`visible` when flattening raw spec params into `ActionParamDef` — so
`ActionParamDialog`'s `filterVisibleParams` never saw the predicate and the
phone field kept rendering even with the phoneNumber auth plugin off.

Propagate `visible` in all three resolve branches (inline / field-backed /
missing-field), unwrapping the spec's `{ dialect, source }` ExpressionInput
envelope to a plain CEL string. Completes the create-user phone fix end to end.
69 changes: 69 additions & 0 deletions packages/app-shell/src/utils/resolveActionParams.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* resolveActionParams — regression coverage for `visible`-predicate propagation.
*
* The create-user phone bug (framework #2871 + objectui #2406) hid the
* `phoneNumber` param via `visible: 'features.phoneNumber == true'`, but the
* resolver dropped `visible` on the way to `ActionParamDialog`, so the field
* kept rendering. These tests lock in that the predicate survives resolution —
* in both the raw-string and the spec-normalised `{ dialect, source }` envelope
* forms, and across inline / field-backed / missing-field branches.
*/
import { describe, it, expect } from 'vitest';
import { resolveActionParams, type ResolveActionParamsContext, type RawActionParam } from './resolveActionParams';

const ctx = (over: Partial<ResolveActionParamsContext> = {}): ResolveActionParamsContext => ({
objectName: 'sys_user',
objects: [
{ name: 'sys_user', fields: { phone_number: { type: 'text', label: 'Phone' } } },
],
fieldLabel: (_o, _f, fallback) => fallback,
...over,
});

describe('resolveActionParams — visible propagation', () => {
it('propagates a raw-string visible on an inline param', () => {
const params: RawActionParam[] = [{ name: 'phoneNumber', visible: 'features.phoneNumber == true' }];
expect(resolveActionParams(params, ctx())[0].visible).toBe('features.phoneNumber == true');
});

it('unwraps the { dialect, source } envelope the spec serialises to', () => {
const params: RawActionParam[] = [
{ name: 'phoneNumber', visible: { dialect: 'cel', source: 'features.phoneNumber == true' } },
];
expect(resolveActionParams(params, ctx())[0].visible).toBe('features.phoneNumber == true');
});

it('propagates visible on a field-backed param', () => {
const params: RawActionParam[] = [{ field: 'phone_number', visible: 'features.phoneNumber == true' }];
expect(resolveActionParams(params, ctx())[0].visible).toBe('features.phoneNumber == true');
});

it('propagates visible on the missing-field fallback branch', () => {
const params: RawActionParam[] = [{ field: 'does_not_exist', visible: 'features.phoneNumber == true' }];
// No such field on the object → text-input fallback, but visible still survives.
expect(resolveActionParams(params, ctx())[0].visible).toBe('features.phoneNumber == true');
});

it('leaves visible undefined when the param has no predicate', () => {
const params: RawActionParam[] = [{ name: 'email' }];
expect(resolveActionParams(params, ctx())[0].visible).toBeUndefined();
});

it('treats an empty predicate as absent (always visible)', () => {
const params: RawActionParam[] = [
{ name: 'a', visible: '' },
{ name: 'b', visible: { dialect: 'cel', source: '' } },
];
const out = resolveActionParams(params, ctx());
expect(out[0].visible).toBeUndefined();
expect(out[1].visible).toBeUndefined();
});
});
26 changes: 26 additions & 0 deletions packages/app-shell/src/utils/resolveActionParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ export interface RawActionParam {
defaultValue?: unknown;
/** When true, seed defaultValue from the row record using the field name. */
defaultFromRow?: boolean;
/**
* Visibility predicate (CEL) — mirrors the spec `ActionParamSchema.visible`.
* The server serialises it through `ExpressionInputSchema` as an
* `{ dialect, source }` envelope, so accept both the raw string and the
* envelope. Propagated to `ActionParamDef.visible` so `ActionParamDialog`
* can gate the param (e.g. hide `phoneNumber` unless `features.phoneNumber`).
* Absent = always visible.
*/
visible?: string | { dialect?: string; source?: string };
}

/** Field metadata as exposed by `useMetadata().objects[].fields`. */
Expand Down Expand Up @@ -109,6 +118,20 @@ function normaliseOptions(
});
}

/**
* Flatten a param `visible` predicate to a plain CEL string. The spec's
* `ExpressionInputSchema` normalises the authored string into an
* `{ dialect, source }` envelope, so unwrap `.source`; a raw string passes
* through untouched. Empty / absent → `undefined` (always visible).
*/
function normaliseVisible(visible: RawActionParam['visible']): string | undefined {
if (typeof visible === 'string') return visible || undefined;
if (visible && typeof visible === 'object' && typeof visible.source === 'string') {
return visible.source || undefined;
}
return undefined;
}

/**
* Resolve a single raw param against object metadata. Inline params pass
* through (with safe defaults); field-backed params inherit from the
Expand Down Expand Up @@ -137,6 +160,7 @@ export function resolveActionParam(
placeholder: param.placeholder,
helpText: param.helpText,
defaultValue: rowDefault ?? param.defaultValue,
visible: normaliseVisible(param.visible),
};
}

Expand All @@ -157,6 +181,7 @@ export function resolveActionParam(
placeholder: param.placeholder,
helpText: param.helpText,
defaultValue: rowDefault ?? param.defaultValue,
visible: normaliseVisible(param.visible),
};
}

Expand Down Expand Up @@ -194,6 +219,7 @@ export function resolveActionParam(
placeholder: param.placeholder ?? field.placeholder,
helpText: param.helpText ?? field.help ?? field.description,
defaultValue: rowDefault ?? param.defaultValue ?? field.defaultValue,
visible: normaliseVisible(param.visible),
...lookupExtras,
};
}
Expand Down
Loading