Skip to content

Commit 71b908d

Browse files
authored
Merge branch 'main' into feat/replace-extraErrorsBlockSubmit-with-extraErrorsAreWarnings
2 parents e6f4aa0 + 1f7541d commit 71b908d

10 files changed

Lines changed: 350 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ should change the heading of the (upcoming) version to include a major version b
2222

2323
- Updated multi-select ArrayFields to properly use the `items` uiSchema for enumerated options, fixing [#4955](https://github.com/rjsf-team/react-jsonschema-form/issues/4955)
2424

25+
## @rjsf/utils
26+
27+
- Fixed `resolveAllReferences` to preserve `$ref` on resolved schemas, enabling `ui:definitions` beyond the first recursion level, fixing [#4966](https://github.com/rjsf-team/react-jsonschema-form/issues/4966)
28+
2529
# 6.3.1
2630

2731
## Dev / docs / playground

packages/core/test/uiSchema.test.tsx

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,4 +2696,173 @@ describe('uiSchema', () => {
26962696
});
26972697
});
26982698
});
2699+
2700+
describe('ui:definitions', () => {
2701+
it('should apply ui:definitions to a simple $ref property', () => {
2702+
const schema: RJSFSchema = {
2703+
type: 'object',
2704+
properties: {
2705+
address: { $ref: '#/$defs/address' },
2706+
},
2707+
$defs: {
2708+
address: {
2709+
type: 'object',
2710+
properties: {
2711+
street: { type: 'string' },
2712+
city: { type: 'string' },
2713+
},
2714+
},
2715+
},
2716+
};
2717+
const uiSchema: UiSchema = {
2718+
'ui:definitions': {
2719+
'#/$defs/address': {
2720+
street: { 'ui:placeholder': 'Enter street' },
2721+
city: { 'ui:widget': 'textarea' },
2722+
},
2723+
},
2724+
};
2725+
2726+
const { node } = createFormComponent({ schema, uiSchema });
2727+
2728+
expect(node.querySelector("input[placeholder='Enter street']")).toBeInTheDocument();
2729+
expect(node.querySelectorAll('textarea')).toHaveLength(1);
2730+
});
2731+
2732+
it('should apply ui:definitions to multiple properties sharing the same $ref', () => {
2733+
const schema: RJSFSchema = {
2734+
type: 'object',
2735+
properties: {
2736+
home: { $ref: '#/$defs/address' },
2737+
work: { $ref: '#/$defs/address' },
2738+
},
2739+
$defs: {
2740+
address: {
2741+
type: 'object',
2742+
properties: {
2743+
street: { type: 'string' },
2744+
},
2745+
},
2746+
},
2747+
};
2748+
const uiSchema: UiSchema = {
2749+
'ui:definitions': {
2750+
'#/$defs/address': {
2751+
street: { 'ui:placeholder': 'Enter street' },
2752+
},
2753+
},
2754+
};
2755+
2756+
const { node } = createFormComponent({ schema, uiSchema });
2757+
2758+
expect(node.querySelectorAll("input[placeholder='Enter street']")).toHaveLength(2);
2759+
});
2760+
2761+
it('should allow local uiSchema to override ui:definitions', () => {
2762+
const schema: RJSFSchema = {
2763+
type: 'object',
2764+
properties: {
2765+
address: { $ref: '#/$defs/address' },
2766+
},
2767+
$defs: {
2768+
address: {
2769+
type: 'object',
2770+
properties: {
2771+
street: { type: 'string' },
2772+
},
2773+
},
2774+
},
2775+
};
2776+
const uiSchema: UiSchema = {
2777+
'ui:definitions': {
2778+
'#/$defs/address': {
2779+
street: { 'ui:placeholder': 'Default street' },
2780+
},
2781+
},
2782+
address: {
2783+
street: { 'ui:placeholder': 'Custom street' },
2784+
},
2785+
};
2786+
2787+
const { node } = createFormComponent({ schema, uiSchema });
2788+
2789+
expect(node.querySelector("input[placeholder='Custom street']")).toBeInTheDocument();
2790+
expect(node.querySelector("input[placeholder='Default street']")).not.toBeInTheDocument();
2791+
});
2792+
2793+
it('should apply ui:definitions to array items with $ref', () => {
2794+
const schema: RJSFSchema = {
2795+
type: 'object',
2796+
properties: {
2797+
people: {
2798+
type: 'array',
2799+
items: { $ref: '#/$defs/person' },
2800+
},
2801+
},
2802+
$defs: {
2803+
person: {
2804+
type: 'object',
2805+
properties: {
2806+
name: { type: 'string' },
2807+
},
2808+
},
2809+
},
2810+
};
2811+
const uiSchema: UiSchema = {
2812+
'ui:definitions': {
2813+
'#/$defs/person': {
2814+
name: { 'ui:placeholder': 'Enter name' },
2815+
},
2816+
},
2817+
};
2818+
2819+
const { node } = createFormComponent({
2820+
schema,
2821+
uiSchema,
2822+
formData: { people: [{ name: 'Alice' }, { name: 'Bob' }] },
2823+
});
2824+
2825+
expect(node.querySelectorAll("input[placeholder='Enter name']")).toHaveLength(2);
2826+
});
2827+
2828+
it('should apply ui:definitions at 5 levels of recursive $ref depth', () => {
2829+
const schema: RJSFSchema = {
2830+
$ref: '#/$defs/node',
2831+
$defs: {
2832+
node: {
2833+
type: 'object',
2834+
properties: {
2835+
name: { type: 'string' },
2836+
children: {
2837+
type: 'array',
2838+
items: { $ref: '#/$defs/node' },
2839+
},
2840+
},
2841+
},
2842+
},
2843+
};
2844+
const uiSchema: UiSchema = {
2845+
'ui:definitions': {
2846+
'#/$defs/node': {
2847+
name: { 'ui:placeholder': 'Node name' },
2848+
},
2849+
},
2850+
};
2851+
2852+
// Build 5-level deep formData: each level has one child
2853+
const formData: GenericObjectType = {
2854+
name: 'L0',
2855+
children: [
2856+
{
2857+
name: 'L1',
2858+
children: [{ name: 'L2', children: [{ name: 'L3', children: [{ name: 'L4', children: [] }] }] }],
2859+
},
2860+
],
2861+
};
2862+
2863+
const { node } = createFormComponent({ schema, uiSchema, formData });
2864+
2865+
expect(node.querySelectorAll("input[placeholder='Node name']")).toHaveLength(5);
2866+
});
2867+
});
26992868
});

packages/utils/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const READONLY_KEY = 'readonly';
2525
export const REQUIRED_KEY = 'required';
2626
export const SUBMIT_BTN_OPTIONS_KEY = 'submitButtonOptions';
2727
export const REF_KEY = '$ref';
28+
export const RJSF_REF_KEY = '__rjsf_ref';
2829
export const SCHEMA_KEY = '$schema';
2930
export const DEFAULT_ID_PREFIX = 'root';
3031
export const DEFAULT_ID_SEPARATOR = '_';

packages/utils/src/isRootSchema.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import isEqual from 'lodash/isEqual';
2+
import omit from 'lodash/omit';
23

34
import { FormContextType, Registry, RJSFSchema, StrictRJSFSchema } from './types';
4-
import { REF_KEY } from './constants';
5+
import { REF_KEY, RJSF_REF_KEY } from './constants';
56

67
/** Helper to check whether a JSON schema object is the root schema. The schema is a root schema with root `properties`
78
* key or a root `$ref` key. If the `schemaToCompare` has a root `oneOf` property, the function will
@@ -24,7 +25,7 @@ export default function isRootSchema<T = any, S extends StrictRJSFSchema = RJSFS
2425
}
2526
if (REF_KEY in rootSchema) {
2627
const resolvedSchema = schemaUtils.retrieveSchema(rootSchema);
27-
return isEqual(schemaToCompare, resolvedSchema);
28+
return isEqual(schemaToCompare, omit(resolvedSchema, RJSF_REF_KEY));
2829
}
2930
return false;
3031
}

packages/utils/src/resolveUiSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ONE_OF_KEY,
77
PROPERTIES_KEY,
88
REF_KEY,
9+
RJSF_REF_KEY,
910
} from './constants';
1011
import findSchemaDefinition from './findSchemaDefinition';
1112
import isObject from './isObject';
@@ -141,7 +142,7 @@ export default function resolveUiSchema<
141142
S extends StrictRJSFSchema = RJSFSchema,
142143
F extends FormContextType = any,
143144
>(schema: S, localUiSchema: UiSchema<T, S, F> | undefined, registry: Registry<T, S, F>): UiSchema<T, S, F> {
144-
const ref = schema[REF_KEY] as string | undefined;
145+
const ref = ((schema as GenericObjectType)[RJSF_REF_KEY] ?? schema[REF_KEY]) as string | undefined;
145146
const definitionUiSchema = ref ? registry.uiSchemaDefinitions?.[ref] : undefined;
146147

147148
if (!definitionUiSchema) {

packages/utils/src/schema/retrieveSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
PATTERN_PROPERTIES_KEY,
2323
PROPERTIES_KEY,
2424
REF_KEY,
25+
RJSF_REF_KEY,
2526
} from '../constants';
2627
import findSchemaDefinition, { splitKeyElementFromObject } from '../findSchemaDefinition';
2728
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema';
@@ -371,7 +372,7 @@ export function resolveAllReferences<S extends StrictRJSFSchema = RJSFSchema>(
371372
recurseList.push($ref!);
372373
// Retrieve the referenced schema definition.
373374
const refSchema = findSchemaDefinition<S>($ref, rootSchema, baseURI);
374-
resolvedSchema = { ...refSchema, ...localSchema };
375+
resolvedSchema = { ...refSchema, ...localSchema, [RJSF_REF_KEY]: $ref };
375376
if (ID_KEY in resolvedSchema) {
376377
baseURI = resolvedSchema[ID_KEY];
377378
}

packages/utils/test/parser/__snapshots__/schemaParser.test.ts.snap

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -857,8 +857,9 @@ exports[`schemaParser() parses schema with oneof and nested dependencies 1`] = `
857857

858858
exports[`schemaParser() parses superSchema properly 1`] = `
859859
{
860-
"-130bae51": {
861-
"$id": "-130bae51",
860+
"-124b5dfb": {
861+
"$id": "-124b5dfb",
862+
"__rjsf_ref": "#/definitions/foo",
862863
"anyOf": [
863864
{
864865
"required": [
@@ -873,33 +874,35 @@ exports[`schemaParser() parses superSchema properly 1`] = `
873874
},
874875
"type": "object",
875876
},
876-
"-1e3c57ec": {
877-
"$id": "-1e3c57ec",
877+
"-42ce06f5": {
878+
"$id": "-42ce06f5",
878879
"anyOf": [
879880
{
880881
"required": [
881-
"choice",
882+
"firstName",
882883
],
883884
},
884885
{
885886
"required": [
886-
"more",
887+
"lastName",
887888
],
888889
},
889890
],
890891
"properties": {
891-
"choice": {
892-
"const": "two",
892+
"firstName": {
893+
"title": "First name",
893894
"type": "string",
894895
},
895-
"more": {
896+
"lastName": {
897+
"__rjsf_ref": "#/definitions/test",
896898
"type": "string",
897899
},
898900
},
899-
"type": "object",
901+
"title": "First method of identification",
900902
},
901-
"-42566fe2": {
902-
"$id": "-42566fe2",
903+
"237b1633": {
904+
"$id": "237b1633",
905+
"__rjsf_ref": "#/definitions/choice2",
903906
"anyOf": [
904907
{
905908
"required": [
@@ -908,61 +911,63 @@ exports[`schemaParser() parses superSchema properly 1`] = `
908911
},
909912
{
910913
"required": [
911-
"other",
914+
"more",
912915
],
913916
},
914917
],
915918
"properties": {
916919
"choice": {
917-
"const": "one",
920+
"const": "two",
918921
"type": "string",
919922
},
920-
"other": {
921-
"type": "number",
923+
"more": {
924+
"type": "string",
922925
},
923926
},
924927
"type": "object",
925928
},
926-
"-7df52035": {
927-
"$id": "-7df52035",
929+
"282df598": {
930+
"$id": "282df598",
928931
"anyOf": [
929932
{
930933
"required": [
931-
"firstName",
932-
],
933-
},
934-
{
935-
"required": [
936-
"lastName",
934+
"idCode",
937935
],
938936
},
939937
],
940938
"properties": {
941-
"firstName": {
942-
"title": "First name",
943-
"type": "string",
944-
},
945-
"lastName": {
939+
"idCode": {
940+
"__rjsf_ref": "#/definitions/test",
946941
"type": "string",
947942
},
948943
},
949-
"title": "First method of identification",
944+
"title": "Second method of identification",
950945
},
951-
"587c7f18": {
952-
"$id": "587c7f18",
946+
"30aa36de": {
947+
"$id": "30aa36de",
948+
"__rjsf_ref": "#/definitions/choice1",
953949
"anyOf": [
954950
{
955951
"required": [
956-
"idCode",
952+
"choice",
953+
],
954+
},
955+
{
956+
"required": [
957+
"other",
957958
],
958959
},
959960
],
960961
"properties": {
961-
"idCode": {
962+
"choice": {
963+
"const": "one",
962964
"type": "string",
963965
},
966+
"other": {
967+
"type": "number",
968+
},
964969
},
965-
"title": "Second method of identification",
970+
"type": "object",
966971
},
967972
"super-schema": {
968973
"$id": "super-schema",

0 commit comments

Comments
 (0)