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
2 changes: 1 addition & 1 deletion packages/stack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/stack",
"version": "2.11.6",
"version": "2.11.7",
"description": "A composable, plugin-based library for building full-stack applications.",
"repository": {
"type": "git",
Expand Down
6 changes: 3 additions & 3 deletions packages/stack/registry/btst-cms.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/stack/registry/btst-form-builder.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,93 @@ describe("jsonSchemaToFieldsAndSteps", () => {
});
});

describe("label resolution", () => {
// Label resolution must mirror what `AutoForm` shows on the public form,
// so a Zod-seeded schema (whose properties only carry `description`,
// not `label`/`title`) reads identically in the admin canvas.
// See `auto-form/fields/object.tsx` → `beautifyObjectName(name)`.

it("uses an explicit `label` when present", () => {
const schema: JSONSchema = {
type: "object",
properties: {
primaryGoal: {
type: "string",
label: "What's your main focus?",
},
},
};

const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
expect(fields[0]?.props.label).toBe("What's your main focus?");
});

it("falls back to JSON Schema `title` when `label` is absent", () => {
const schema: JSONSchema = {
type: "object",
properties: {
primaryGoal: {
type: "string",
title: "Primary Goal (titled)",
},
},
};

const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
expect(fields[0]?.props.label).toBe("Primary Goal (titled)");
});

it("humanises the field key when no `label` or `title` is present", () => {
// This is the regression: previously the admin showed the raw key
// (`primaryGoal`) when no label/title was set, while the public form
// showed `Primary Goal`. Now both paths agree.
const schema: JSONSchema = {
type: "object",
properties: {
primaryGoal: { type: "string" },
riskTolerance: {
type: "string",
enum: ["low", "moderate", "high"],
},
noInjections: { type: "boolean", fieldType: "switch" },
email: { type: "string", format: "email" },
age: { type: "number" },
notes: { type: "string", fieldType: "textarea" },
},
};

const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
const labels = Object.fromEntries(
fields.map((f) => [f.id, f.props.label]),
);

expect(labels).toEqual({
primaryGoal: "Primary Goal",
riskTolerance: "Risk Tolerance",
noInjections: "No Injections",
email: "Email",
age: "Age",
notes: "Notes",
});
});

it("humanises the field key in the unknown-field-type fallback", () => {
// When no component matches, propertiesToFields creates a generic
// text field. That branch must humanise the key too.
const schema: JSONSchema = {
type: "object",
properties: {
mysteriousField: {
// no `type` — falls through to the generic text fallback
} as JSONSchema["properties"][string],
},
};

const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
expect(fields[0]?.props.label).toBe("Mysterious Field");
});
});

describe("multi-step: Path A — per-property stepGroup", () => {
// Canonical format written by the visual FormBuilder when saving.
const schema: JSONSchema = {
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/src/components/form-builder/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
arrayValidationSchema,
DEFAULT_VALUE_SCHEMAS,
} from "../validation-schemas";
import { beautifyObjectName } from "../../auto-form/helpers";

/**
* Helper to convert a value to a number, handling empty strings and undefined
Expand Down Expand Up @@ -70,10 +71,20 @@ function getPlaceholder(prop: JSONSchemaProperty): string | undefined {
}

/**
* Helper to extract label from JSONSchemaProperty
* Helper to extract a display label from a JSONSchemaProperty.
*
* Resolution order:
* 1. Explicit `prop.label` (set via the visual builder's edit dialog).
* 2. Standard JSON Schema `prop.title`.
* 3. Humanised field key — `beautifyObjectName("primaryGoal")` → `"Primary Goal"`.
*
* The humanised key fallback matches what `AutoForm` renders on the public
* form (see `auto-form/fields/object.tsx`), so a Zod-seeded form whose
* properties only carry a `description` (no `label`/`title`) reads the same
* in the admin canvas as it does to end users.
*/
function getLabel(prop: JSONSchemaProperty, key: string): string {
return prop.label || prop.title || key;
return prop.label || prop.title || beautifyObjectName(key);
}

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/src/components/form-builder/schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
JSONSchema,
JSONSchemaProperty,
} from "./types";
import { beautifyObjectName } from "../auto-form/helpers";

/**
* Helper to convert fields to JSON Schema properties (recursive)
Expand Down Expand Up @@ -152,13 +153,15 @@ function propertiesToFields(

fields.push(field);
} else {
// Fallback: create a generic text field for unknown types
// Fallback: create a generic text field for unknown types.
// Label resolution mirrors `getLabel` in `./components/index.tsx` so the
// admin canvas always shows a humanised label for unknown shapes too.
console.warn(`Could not parse JSON Schema property: ${key}`, prop);
fields.push({
id: key,
type: "text",
props: {
label: prop.title || key,
label: prop.label || prop.title || beautifyObjectName(key),
description: prop.description,
placeholder: prop.placeholder,
required: isRequired,
Expand Down