From 66c6f5f6f89c0472976958f95359be2d2170f4f7 Mon Sep 17 00:00:00 2001 From: Kostiantyn Dvornik Date: Thu, 25 Jun 2026 18:49:21 +0300 Subject: [PATCH 1/3] Fix points grid form schema so RJSF does not show dimensions anyOf picker. Replace dimensions after schema patching in buildFormJsonSchema(), since patch merge leaves ESSE anyOf intact and RJSF renders it as a branch selector. Co-authored-by: Cursor --- .../PointsGrid/IGridFormDataManager.js | 7 +- .../PointsGrid/KGridFormDataManager.js | 7 +- .../PointsGridFormDataProvider.d.ts | 15 ++--- .../PointsGrid/PointsGridFormDataProvider.js | 14 +++- .../PointsGrid/QGridFormDataManager.js | 7 +- .../PointsGrid/IGridFormDataManager.ts | 12 +--- .../PointsGrid/KGridFormDataManager.ts | 12 +--- .../PointsGrid/PointsGridFormDataProvider.ts | 28 +++++++- .../PointsGrid/QGridFormDataManager.ts | 12 +--- tests/js/pointsGridFormDataProvider.test.ts | 67 +++++++++++++++++++ 10 files changed, 117 insertions(+), 64 deletions(-) create mode 100644 tests/js/pointsGridFormDataProvider.test.ts diff --git a/dist/js/context/providers/PointsGrid/IGridFormDataManager.js b/dist/js/context/providers/PointsGrid/IGridFormDataManager.js index de182bc7..61c94be9 100644 --- a/dist/js/context/providers/PointsGrid/IGridFormDataManager.js +++ b/dist/js/context/providers/PointsGrid/IGridFormDataManager.js @@ -3,17 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); const PointsGridFormDataProvider_1 = __importDefault(require("./PointsGridFormDataProvider")); class IGridFormDataManager extends PointsGridFormDataProvider_1.default { constructor(contextItem, externalContext) { super(contextItem, externalContext, 0.2); this.name = "igrid"; - const jsonSchema = JSONSchemasInterface_1.default.getPatchedSchemaById(this.jsonSchemaId, this.jsonSchemaPatchConfig); - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext, externalContext) { const contextItem = this.findContextItem(unitContext, "igrid"); diff --git a/dist/js/context/providers/PointsGrid/KGridFormDataManager.js b/dist/js/context/providers/PointsGrid/KGridFormDataManager.js index 9a862fd6..688e4a4b 100644 --- a/dist/js/context/providers/PointsGrid/KGridFormDataManager.js +++ b/dist/js/context/providers/PointsGrid/KGridFormDataManager.js @@ -3,17 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); const PointsGridFormDataProvider_1 = __importDefault(require("./PointsGridFormDataProvider")); class KGridFormDataManager extends PointsGridFormDataProvider_1.default { constructor(contextItem, externalContext) { super(contextItem, externalContext, 1); this.name = "kgrid"; - const jsonSchema = JSONSchemasInterface_1.default.getPatchedSchemaById(this.jsonSchemaId, this.jsonSchemaPatchConfig); - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext, externalContext) { const contextItem = this.findContextItem(unitContext, "kgrid"); diff --git a/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.d.ts b/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.d.ts index bb1d3908..fa92b328 100644 --- a/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.d.ts +++ b/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.d.ts @@ -36,16 +36,6 @@ declare abstract class PointsGridFormDataProvider exte getData(): Data; getDefaultData(): PointsGridDataProviderSchema; protected get jsonSchemaPatchConfig(): { - dimensions: { - default?: any[] | undefined; - type: string; - items: { - default?: string | number | readonly number[] | readonly string[] | undefined; - type: string; - }; - minItems: number; - maxItems: number; - }; shifts: { default?: any[] | undefined; type: string; @@ -113,6 +103,11 @@ declare abstract class PointsGridFormDataProvider exte }; }; }; + /** + * Form schema for RJSF. Replaces ESSE `dimensions.anyOf` (number[] | string[]) with a single + * array type — patch merge cannot remove `anyOf`, which makes RJSF render a branch picker. + */ + protected buildFormJsonSchema(): JSONSchema7; /** Prefer persisted `data` — `setData` runs before React re-inits the provider on render. */ private get preferGridMetricForUi(); get uiSchema(): { diff --git a/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.js b/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.js index c9e345e9..5c1e2e9a 100644 --- a/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.js +++ b/dist/js/context/providers/PointsGrid/PointsGridFormDataProvider.js @@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const constants_1 = require("@mat3ra/code/dist/js/constants"); const math_1 = require("@mat3ra/code/dist/js/math"); +const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); const made_1 = require("@mat3ra/made"); const MaterialContextMixin_1 = __importDefault(require("../../mixins/MaterialContextMixin")); const JSONSchemaFormDataProvider_1 = __importDefault(require("../base/JSONSchemaFormDataProvider")); @@ -112,7 +113,6 @@ class PointsGridFormDataProvider extends JSONSchemaFormDataProvider_1.default { }; const gridMetricType = ((_a = this.data) === null || _a === void 0 ? void 0 : _a.gridMetricType) || this.defaultMetric.type; return { - dimensions: vector(this.defaultDimensions, this.isUsingJinjaVariables), shifts: vector(defaultShifts), reciprocalVectorRatios: vector(this.reciprocalVectorRatios), gridMetricType: { default: this.defaultMetric.type }, @@ -158,6 +158,18 @@ class PointsGridFormDataProvider extends JSONSchemaFormDataProvider_1.default { }, }; } + /** + * Form schema for RJSF. Replaces ESSE `dimensions.anyOf` (number[] | string[]) with a single + * array type — patch merge cannot remove `anyOf`, which makes RJSF render a branch picker. + */ + buildFormJsonSchema() { + const jsonSchema = JSONSchemasInterface_1.default.getPatchedSchemaById(this.jsonSchemaId, this.jsonSchemaPatchConfig); + if (!(jsonSchema === null || jsonSchema === void 0 ? void 0 : jsonSchema.properties)) { + throw new Error("Failed to get patched JSON schema"); + } + jsonSchema.properties.dimensions = vector(this.defaultDimensions, this.isUsingJinjaVariables); + return jsonSchema; + } /** Prefer persisted `data` — `setData` runs before React re-inits the provider on render. */ get preferGridMetricForUi() { var _a, _b; diff --git a/dist/js/context/providers/PointsGrid/QGridFormDataManager.js b/dist/js/context/providers/PointsGrid/QGridFormDataManager.js index 0b3ee873..d440a010 100644 --- a/dist/js/context/providers/PointsGrid/QGridFormDataManager.js +++ b/dist/js/context/providers/PointsGrid/QGridFormDataManager.js @@ -3,17 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const JSONSchemasInterface_1 = __importDefault(require("@mat3ra/esse/dist/js/esse/JSONSchemasInterface")); const PointsGridFormDataProvider_1 = __importDefault(require("./PointsGridFormDataProvider")); class QGridFormDataManager extends PointsGridFormDataProvider_1.default { constructor(contextItem, externalContext) { super(contextItem, externalContext, 5); this.name = "qgrid"; - const jsonSchema = JSONSchemasInterface_1.default.getPatchedSchemaById(this.jsonSchemaId, this.jsonSchemaPatchConfig); - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext, externalContext) { const contextItem = this.findContextItem(unitContext, "qgrid"); diff --git a/src/js/context/providers/PointsGrid/IGridFormDataManager.ts b/src/js/context/providers/PointsGrid/IGridFormDataManager.ts index b9263d7c..b6ecb3c6 100644 --- a/src/js/context/providers/PointsGrid/IGridFormDataManager.ts +++ b/src/js/context/providers/PointsGrid/IGridFormDataManager.ts @@ -1,4 +1,3 @@ -import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface"; import type { GridContextItemSchema } from "@mat3ra/esse/dist/js/types"; import type { JSONSchema7 } from "json-schema"; @@ -16,16 +15,7 @@ export default class IGridFormDataManager extends PointsGridFormDataProvider, externalContext: ExternalContext) { super(contextItem, externalContext, 0.2); - const jsonSchema = JSONSchemasInterface.getPatchedSchemaById( - this.jsonSchemaId, - this.jsonSchemaPatchConfig, - ); - - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext: UnitContext, externalContext: ExternalContext) { diff --git a/src/js/context/providers/PointsGrid/KGridFormDataManager.ts b/src/js/context/providers/PointsGrid/KGridFormDataManager.ts index 4c7e9c71..8973314e 100644 --- a/src/js/context/providers/PointsGrid/KGridFormDataManager.ts +++ b/src/js/context/providers/PointsGrid/KGridFormDataManager.ts @@ -1,4 +1,3 @@ -import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface"; import type { GridContextItemSchema } from "@mat3ra/esse/dist/js/types"; import type { JSONSchema7 } from "json-schema"; @@ -17,16 +16,7 @@ export default class KGridFormDataManager extends PointsGridFormDataProvider, externalContext: ExternalContext) { super(contextItem, externalContext, 1); - const jsonSchema = JSONSchemasInterface.getPatchedSchemaById( - this.jsonSchemaId, - this.jsonSchemaPatchConfig, - ); - - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext: UnitContext, externalContext: ExternalContext) { diff --git a/src/js/context/providers/PointsGrid/PointsGridFormDataProvider.ts b/src/js/context/providers/PointsGrid/PointsGridFormDataProvider.ts index 386cd37b..b870280f 100644 --- a/src/js/context/providers/PointsGrid/PointsGridFormDataProvider.ts +++ b/src/js/context/providers/PointsGrid/PointsGridFormDataProvider.ts @@ -1,13 +1,14 @@ import { Units } from "@mat3ra/code/dist/js/constants"; import { math as codeJSMath } from "@mat3ra/code/dist/js/math"; import type { Constructor } from "@mat3ra/code/dist/js/utils/types"; +import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface"; import type { GridContextItemSchema, PointsGridDataProviderSchema, Vector3DSchema, } from "@mat3ra/esse/dist/js/types"; import { type ReciprocalLattice, Made } from "@mat3ra/made"; -import type { JSONSchema7 } from "json-schema"; +import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import materialContextMixin, { type MaterialContextMixin, @@ -81,6 +82,8 @@ abstract class PointsGridFormDataProvider< value: number; }; + // Assigned in subclass constructors via buildFormJsonSchema() — not in this constructor: + // jsonSchemaPatchConfig uses this.name, which is only set after super() returns. abstract readonly jsonSchema: JSONSchema7; constructor(contextItem: Partial, externalContext: ExternalContext, divisor: number) { @@ -186,7 +189,6 @@ abstract class PointsGridFormDataProvider< const gridMetricType = this.data?.gridMetricType || this.defaultMetric.type; return { - dimensions: vector(this.defaultDimensions, this.isUsingJinjaVariables), shifts: vector(defaultShifts), reciprocalVectorRatios: vector(this.reciprocalVectorRatios), gridMetricType: { default: this.defaultMetric.type }, @@ -235,6 +237,28 @@ abstract class PointsGridFormDataProvider< }; } + /** + * Form schema for RJSF. Replaces ESSE `dimensions.anyOf` (number[] | string[]) with a single + * array type — patch merge cannot remove `anyOf`, which makes RJSF render a branch picker. + */ + protected buildFormJsonSchema(): JSONSchema7 { + const jsonSchema = JSONSchemasInterface.getPatchedSchemaById( + this.jsonSchemaId, + this.jsonSchemaPatchConfig, + ); + + if (!jsonSchema?.properties) { + throw new Error("Failed to get patched JSON schema"); + } + + jsonSchema.properties.dimensions = vector( + this.defaultDimensions, + this.isUsingJinjaVariables, + ) as JSONSchema7Definition; + + return jsonSchema; + } + /** Prefer persisted `data` — `setData` runs before React re-inits the provider on render. */ private get preferGridMetricForUi() { return this.data?.preferGridMetric ?? this.preferGridMetric; diff --git a/src/js/context/providers/PointsGrid/QGridFormDataManager.ts b/src/js/context/providers/PointsGrid/QGridFormDataManager.ts index 39ed09a1..05c068c8 100644 --- a/src/js/context/providers/PointsGrid/QGridFormDataManager.ts +++ b/src/js/context/providers/PointsGrid/QGridFormDataManager.ts @@ -1,4 +1,3 @@ -import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface"; import type { GridContextItemSchema } from "@mat3ra/esse/dist/js/types"; import type { JSONSchema7 } from "json-schema"; @@ -16,16 +15,7 @@ export default class QGridFormDataManager extends PointsGridFormDataProvider, externalContext: ExternalContext) { super(contextItem, externalContext, 5); - const jsonSchema = JSONSchemasInterface.getPatchedSchemaById( - this.jsonSchemaId, - this.jsonSchemaPatchConfig, - ); - - if (!jsonSchema) { - throw new Error("Failed to get patched JSON schema"); - } - - this.jsonSchema = jsonSchema; + this.jsonSchema = this.buildFormJsonSchema(); } static createFromUnitContext(unitContext: UnitContext, externalContext: ExternalContext) { diff --git a/tests/js/pointsGridFormDataProvider.test.ts b/tests/js/pointsGridFormDataProvider.test.ts new file mode 100644 index 00000000..34404f5a --- /dev/null +++ b/tests/js/pointsGridFormDataProvider.test.ts @@ -0,0 +1,67 @@ +import { + type InMemoryEntityInSet, + inMemoryEntityInSetMixin, +} from "@mat3ra/code/dist/js/entity/set/InMemoryEntityInSetMixin"; +import { + type OrderedInMemoryEntityInSet, + orderedEntityInSetMixin, +} from "@mat3ra/code/dist/js/entity/set/ordered/OrderedInMemoryEntityInSetMixin"; +import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface"; +import esseSchemas from "@mat3ra/esse/dist/js/schemas.json"; +import { Material } from "@mat3ra/made"; +import { expect } from "chai"; +import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; + +import KGridFormDataManager from "../../src/js/context/providers/PointsGrid/KGridFormDataManager"; + +interface OrderedMaterial extends OrderedInMemoryEntityInSet, InMemoryEntityInSet {} + +class OrderedMaterial extends Material implements OrderedInMemoryEntityInSet { + declare static createDefault: () => OrderedMaterial; +} + +inMemoryEntityInSetMixin(OrderedMaterial.prototype); +orderedEntityInSetMixin(OrderedMaterial.prototype); + +function getDimensionsSchema(jsonSchema: JSONSchema7): JSONSchema7Definition { + const dimensions = jsonSchema.properties?.dimensions; + expect(dimensions).to.be.an("object"); + return dimensions as JSONSchema7Definition; +} + +describe("PointsGridFormDataProvider form jsonSchema", () => { + before(() => { + JSONSchemasInterface.setSchemas(esseSchemas as JSONSchema7[]); + }); + + it("exposes number[] dimensions without anyOf for RJSF", () => { + const material = OrderedMaterial.createDefault(); + material.hash = material.calculateHash(); + + const provider = new KGridFormDataManager( + { name: "kgrid" }, + { material, isUsingJinjaVariables: false }, + ); + + const dimensionsSchema = getDimensionsSchema(provider.jsonSchema); + + expect(dimensionsSchema).to.not.have.property("anyOf"); + expect(dimensionsSchema).to.deep.include({ type: "array", minItems: 3, maxItems: 3 }); + expect((dimensionsSchema as JSONSchema7).items).to.deep.include({ type: "number" }); + }); + + it("exposes string[] dimensions when using Jinja variables", () => { + const material = OrderedMaterial.createDefault(); + material.hash = material.calculateHash(); + + const provider = new KGridFormDataManager( + { name: "kgrid" }, + { material, isUsingJinjaVariables: true }, + ); + + const dimensionsSchema = getDimensionsSchema(provider.jsonSchema); + + expect(dimensionsSchema).to.not.have.property("anyOf"); + expect((dimensionsSchema as JSONSchema7).items).to.deep.include({ type: "string" }); + }); +}); From f1cd712cdcec30dc8bbfbef1e983332900e58de4 Mon Sep 17 00:00:00 2001 From: Kostiantyn Dvornik Date: Thu, 25 Jun 2026 18:51:53 +0300 Subject: [PATCH 2/3] Increase standata integration test timeout for slower CI runners. The all-workflows render loop can exceed Mocha's default 2s limit on GitHub Actions. Co-authored-by: Cursor --- tests/js/subworkflow.standata.integration.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/js/subworkflow.standata.integration.test.ts b/tests/js/subworkflow.standata.integration.test.ts index 3cd8a540..4f06774d 100644 --- a/tests/js/subworkflow.standata.integration.test.ts +++ b/tests/js/subworkflow.standata.integration.test.ts @@ -39,7 +39,9 @@ describe("Subworkflow", () => { ApplicationRegistry.setDriver(new StandataDriver()); }); - it("addConvergence on first subworkflow then workflow.render for every standata workflow (when applicable)", () => { + it("addConvergence on first subworkflow then workflow.render for every standata workflow (when applicable)", function () { + this.timeout(10000); + const standataWorkflows = new WorkflowStandata().getAll() as unknown as WorkflowSchema[]; expect(standataWorkflows.length).to.be.above(0); From 137df0e6df1584405bfcf20a73638670fbd6c928 Mon Sep 17 00:00:00 2001 From: Kostiantyn Dvornik Date: Thu, 25 Jun 2026 18:53:50 +0300 Subject: [PATCH 3/3] Document why standata integration test uses a longer Mocha timeout. Co-authored-by: Cursor --- tests/js/subworkflow.standata.integration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/js/subworkflow.standata.integration.test.ts b/tests/js/subworkflow.standata.integration.test.ts index 4f06774d..dd675aba 100644 --- a/tests/js/subworkflow.standata.integration.test.ts +++ b/tests/js/subworkflow.standata.integration.test.ts @@ -40,6 +40,7 @@ describe("Subworkflow", () => { }); it("addConvergence on first subworkflow then workflow.render for every standata workflow (when applicable)", function () { + // Renders every standata workflow; ~1s locally but exceeds Mocha's default 2s on GitHub Actions. this.timeout(10000); const standataWorkflows = new WorkflowStandata().getAll() as unknown as WorkflowSchema[];