@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
821 lines (820 loc) • 34.2 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* ========================================================================
* ARCHITECTURE: DataFormUtilities
* ========================================================================
*
* DataFormUtilities provides static utility functions for the DataForm
* system, including field selection, type checking, and form manipulation.
*
* This file is a key part of the "upscale/downscale" UI layer pattern.
* See FormPropertyManager.ts for the complete architecture documentation.
*
* KEY FUNCTION: selectFieldForValue()
*
* This function implements the UI-layer part of the upscale pattern.
* When a field has multiple "alternates" (different type representations),
* this function selects the variant that best matches the actual data.
*
* Example: A repair_items field might have:
* - Primary: stringArray (for simple item list)
* - Alternate: objectArray with subForm (for complex items with amounts)
*
* If the data is an array of objects, selectFieldForValue returns the
* objectArray alternate so the UI renders the appropriate editor.
*
* RELATED FILES:
* - src/dataform/FormPropertyManager.ts - Data-layer upscale/downscale
* - src/dataformux/DataForm.tsx - Uses selectFieldForValue() in render
* - src/dataform/IField.ts - Field definitions with alternates
*
* ========================================================================
*/
const Utilities_1 = __importDefault(require("../core/Utilities"));
const Database_1 = __importDefault(require("../minecraft/Database"));
const CreatorToolsHost_1 = __importDefault(require("../app/CreatorToolsHost"));
const IField_1 = require("./IField");
const SummarizerEvaluator_1 = __importDefault(require("./SummarizerEvaluator"));
class DataFormUtilities {
static generateDefaultItem(formDefinition) {
const newDataObject = {};
for (let i = 0; i < formDefinition.fields.length; i++) {
const field = formDefinition.fields[i];
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string" &&
DataFormUtilities.isObjectFieldType(field.dataType) &&
Utilities_1.default.isUsableAsObjectKey(field.id)) {
// sometimes our docs say the default value for an object is "N/A", which is not awesome
newDataObject[field.id] = {};
}
else {
newDataObject[field.id] = field.defaultValue;
}
}
}
return newDataObject;
}
/**
* Returns true if the component data only contains default or empty values
* relative to the form definition — meaning it can be safely removed from
* the persisted JSON without losing any user intent.
*/
static isDefaultOrEmpty(formDefinition, data) {
if (data === undefined || data === null) {
return true;
}
if (typeof data !== "object") {
return false;
}
const record = data;
for (const field of formDefinition.fields) {
const actual = record[field.id];
if (actual === undefined || actual === null) {
continue;
}
if (field.defaultValue !== undefined) {
if (typeof actual === "object" && typeof field.defaultValue === "object") {
if (JSON.stringify(actual) !== JSON.stringify(field.defaultValue)) {
return false;
}
}
else if (actual !== field.defaultValue) {
return false;
}
}
else {
if (typeof actual === "object") {
if (Array.isArray(actual) ? actual.length > 0 : Object.keys(actual).length > 0) {
return false;
}
}
else if (actual !== "" && actual !== 0 && actual !== false) {
return false;
}
}
}
const fieldIds = new Set(formDefinition.fields.map((f) => f.id));
for (const key of Object.keys(record)) {
if (!fieldIds.has(key) && record[key] !== undefined && record[key] !== null) {
return false;
}
}
return true;
}
static selectSubForm(form, select) {
const selectors = select.split("/");
for (const selector of selectors) {
if (selector.length > 0) {
const field = DataFormUtilities.getFieldById(form, selector);
if (!field || !field.subForm) {
throw new Error("Unable to find field " + selector + " in form " + form.id);
}
form = field.subForm;
}
}
return form;
}
static mergeFields(form) {
let fields = [];
const fieldsByName = {};
for (const field of form.fields) {
if (fieldsByName[field.id]) {
const origField = fieldsByName[field.id];
if (!origField.alternates) {
origField.alternates = [];
}
origField.alternates.push(field);
if (field.alternates) {
for (const subField of field.alternates) {
origField.alternates.push(subField);
}
field.alternates = undefined;
}
}
else {
fields.push(field);
if (Utilities_1.default.isUsableAsObjectKey(field.id)) {
fieldsByName[field.id] = field;
}
}
}
form.fields = fields;
for (const field of fields) {
if (field.subForm) {
DataFormUtilities.mergeFields(field.subForm);
}
if (field.alternates) {
for (let addField of field.alternates) {
if (addField.subForm) {
DataFormUtilities.mergeFields(addField.subForm);
}
}
}
}
DataFormUtilities.sortAndCleanAlternateFields(form);
}
/**
* Selects the most appropriate field definition (primary or alternate) based on the actual value type.
*
* This is the UI-layer component of the "upscale/downscale" pattern. It ensures that
* when rendering a form, the correct editor is used for the actual data type.
*
* BACKGROUND:
* Minecraft JSON often supports multiple representations of the same field:
* - A scalar: "hello"
* - An array: ["a", "b"]
* - An object: { items: ["a"], amount: 5 }
*
* Field definitions can have "alternates" - additional field definitions that
* represent the same logical field but with different dataTypes and potentially
* different subForms.
*
* HOW IT WORKS:
* 1. Collects all field variants (primary + alternates)
* 2. Scores each variant based on how well its dataType matches the value
* 3. Returns the highest-scoring variant
*
* SCORING RULES:
* - Array values prefer stringArray, numberArray, objectArray types
* - Object values prefer object types, especially with subForms
* - Scalar values prefer matching primitive types (string, int, boolean)
*
* USAGE IN DataForm.tsx:
* const effectiveField = DataFormUtilities.selectFieldForValue(field, curVal);
* // effectiveField is now the variant that best represents curVal
* // Use effectiveField.dataType, effectiveField.subForm, etc.
*
* RELATED:
* - FormPropertyManager.upscaleDirectObject() - Data-layer scalar→object
* - FormPropertyManager.downscaleDirectObject() - Data-layer object→scalar
*
* @param field - The primary field definition (may have alternates)
* @param value - The actual value to match against
* @returns The best-matching field definition (primary or an alternate)
*/
static selectFieldForValue(field, value) {
if (value === undefined || value === null) {
return field;
}
// Collect all field variants (primary + alternates)
const allFields = [field];
if (field.alternates) {
allFields.push(...field.alternates);
}
// If only one field, return it
if (allFields.length === 1) {
return field;
}
const valueType = typeof value;
const isArray = Array.isArray(value);
const isObject = valueType === "object" && !isArray;
// Score each field based on how well it matches the value type
let bestField = field;
let bestScore = -1;
for (const candidate of allFields) {
let score = 0;
const dt = candidate.dataType;
if (isArray) {
// Value is an array - prefer array types
if (dt === IField_1.FieldDataType.stringArray ||
dt === IField_1.FieldDataType.numberArray ||
dt === IField_1.FieldDataType.objectArray ||
dt === IField_1.FieldDataType.longFormStringArray) {
score = 10;
}
}
else if (isObject) {
// Value is an object - prefer object types (especially those with subForms)
if (DataFormUtilities.isObjectFieldType(dt)) {
score = 10;
if (candidate.subForm || candidate.subFormId) {
score = 15; // Prefer objects with subForms
}
}
else if (dt === IField_1.FieldDataType.keyedObjectCollection || dt === IField_1.FieldDataType.keyedStringCollection) {
score = 8;
}
}
else if (valueType === "string") {
// Value is a string - prefer string types
// Note: isString includes stringEnum, so it gets score 10
if (DataFormUtilities.isString(dt)) {
score = 10;
}
}
else if (valueType === "number") {
// Value is a number - prefer numeric types
if (dt === IField_1.FieldDataType.int ||
dt === IField_1.FieldDataType.float ||
dt === IField_1.FieldDataType.number ||
dt === IField_1.FieldDataType.long) {
score = 10;
}
else if (dt === IField_1.FieldDataType.intEnum) {
score = 9;
}
}
else if (valueType === "boolean") {
// Value is a boolean - prefer boolean types
if (dt === IField_1.FieldDataType.boolean || dt === IField_1.FieldDataType.intBoolean) {
score = 10;
}
}
if (score > bestScore) {
bestScore = score;
bestField = candidate;
}
}
return bestField;
}
static isObjectFieldType(fieldDataType) {
return (fieldDataType === IField_1.FieldDataType.object ||
fieldDataType === IField_1.FieldDataType.minecraftFilter ||
fieldDataType === IField_1.FieldDataType.minecraftEventTrigger);
}
static isScalarFieldType(fieldDataType) {
return (fieldDataType === IField_1.FieldDataType.boolean ||
fieldDataType === IField_1.FieldDataType.float ||
fieldDataType === IField_1.FieldDataType.int ||
fieldDataType === IField_1.FieldDataType.intBoolean ||
fieldDataType === IField_1.FieldDataType.intEnum ||
fieldDataType === IField_1.FieldDataType.string ||
fieldDataType === IField_1.FieldDataType.molang);
}
static sortFieldsBySignificance(fieldA, fieldB) {
if (fieldA.subForm && !fieldB.subForm) {
return -1;
}
if (!fieldA.subForm && fieldB.subForm) {
return 1;
}
const isAComplex = !DataFormUtilities.isScalarFieldType(fieldA.dataType);
const isBComplex = !DataFormUtilities.isScalarFieldType(fieldB.dataType);
if (isAComplex && !isBComplex) {
return -1;
}
if (!isAComplex && isBComplex) {
return 1;
}
if (fieldA.dataType < fieldB.dataType) {
return -1;
}
if (fieldA.dataType > fieldB.dataType) {
return 1;
}
return 0;
}
static sortFieldsByPriority(fieldA, fieldB) {
if (fieldA.isDeprecated && !fieldB.isDeprecated) {
return 1;
}
if (!fieldA.isDeprecated && fieldB.isDeprecated) {
return -1;
}
if (fieldA.priority && !fieldB.priority) {
return -1;
}
if (fieldB.priority && !fieldA.priority) {
return 1;
}
if (fieldA.priority && fieldB.priority) {
return fieldA.priority - fieldB.priority;
}
if (fieldA.title && fieldB.title) {
return fieldA.title.localeCompare(fieldB.title);
}
if (fieldA.id && fieldB.id) {
return Utilities_1.default.staticCompare(fieldA.id, fieldB.id);
}
return 0;
}
static async loadSubForms(form, loadedForms) {
if (!loadedForms) {
loadedForms = "";
}
if (form && form.fields) {
for (const field of form.fields) {
let subForm = undefined;
if (field.subForm) {
subForm = field.subForm;
}
else if (field.subFormId) {
if (loadedForms.indexOf("|" + field.subFormId + "|") < 0) {
subForm = await Database_1.default.ensureFormLoadedByPath(field.subFormId);
loadedForms += "|" + field.subFormId + "|";
}
}
if (subForm) {
await this.loadSubForms(subForm, loadedForms);
}
}
}
if (loadedForms === "") {
return undefined;
}
return loadedForms;
}
static sortAndCleanAlternateFields(form) {
let fields = [];
for (const field of form.fields) {
if (field.alternates) {
const allFields = [];
allFields.push(field);
allFields.push(...field.alternates);
field.alternates = undefined;
allFields.sort(DataFormUtilities.sortFieldsBySignificance);
allFields[0].alternates = [];
fields.push(allFields[0]);
for (let i = 1; i < allFields.length; i++) {
if (allFields[i].title === allFields[0].title) {
allFields[i].title = undefined;
}
allFields[0].alternates.push(allFields[i]);
}
}
else {
fields.push(field);
}
}
form.fields = fields;
for (const field of fields) {
if (field.subForm) {
DataFormUtilities.sortAndCleanAlternateFields(field.subForm);
}
if (field.alternates) {
for (let addField of field.alternates) {
if (addField.subForm) {
DataFormUtilities.sortAndCleanAlternateFields(addField.subForm);
}
}
}
}
}
static fixupFields(form, parentField) {
let fields = [];
for (const field of form.fields) {
if ((field.dataType === IField_1.FieldDataType.stringArray || field.dataType === IField_1.FieldDataType.numberArray) &&
field.subForm &&
field.subForm.fields &&
field.subForm.fields.length >= 1) {
const subField = field.subForm.fields[0];
if (subField.id.indexOf("<") >= 0) {
if (subField.dataType === IField_1.FieldDataType.molang) {
field.dataType = IField_1.FieldDataType.molangArray;
}
field.subForm = undefined;
}
fields.push(field);
}
else if (field.id.startsWith("<") && parentField) {
if (!parentField.alternates) {
parentField.alternates = [];
}
field.keyDescription = field.id;
field.id = parentField.id;
if (field.dataType === IField_1.FieldDataType.molangArray || field.dataType === IField_1.FieldDataType.stringArray) {
field.dataType = IField_1.FieldDataType.keyedStringCollection;
}
parentField.alternates.push(field);
}
else {
fields.push(field);
}
}
form.fields = fields;
for (const field of fields) {
if (field.subForm) {
DataFormUtilities.fixupFields(field.subForm, field);
}
if (field.alternates) {
for (let addField of field.alternates) {
if (addField.subForm) {
DataFormUtilities.fixupFields(addField.subForm, field);
}
}
}
}
}
static generateFormFromObject(id, obj, exampleSourcePath) {
let fields = [];
for (const fieldName in obj) {
const fieldData = obj[fieldName];
let fieldType = IField_1.FieldDataType.string;
if (typeof fieldData === "number") {
fieldType = IField_1.FieldDataType.number;
}
const samples = {};
samples[exampleSourcePath ? exampleSourcePath : "generated_doNotEdit"] = [
{
path: fieldName,
content: fieldData,
},
];
fields.push({
id: fieldName,
title: Utilities_1.default.humanifyJsName(fieldName),
dataType: fieldType,
samples: samples,
});
}
return {
id: id,
title: Utilities_1.default.humanifyJsName(id),
fields: fields,
};
}
static getFieldAndAlternates(fieldDefinition) {
const fields = [fieldDefinition];
if (fieldDefinition.alternates) {
for (const altField of fieldDefinition.alternates) {
fields.push(...DataFormUtilities.getFieldAndAlternates(altField));
}
}
return fields;
}
static getScalarField(formDefinition) {
if (formDefinition.scalarField) {
return formDefinition.scalarField;
}
if (formDefinition.scalarFieldUpgradeName && formDefinition.fields) {
for (const field of formDefinition.fields) {
if (field.id === formDefinition.scalarFieldUpgradeName) {
return field;
}
}
}
return undefined;
}
static isString(fieldType) {
return (fieldType === IField_1.FieldDataType.string ||
fieldType === IField_1.FieldDataType.molang ||
fieldType === IField_1.FieldDataType.longFormString ||
fieldType === IField_1.FieldDataType.stringLookup ||
fieldType === IField_1.FieldDataType.stringEnum ||
fieldType === IField_1.FieldDataType.localizableString);
}
static getFieldById(formDefinition, fieldId) {
if (!formDefinition.fields) {
return undefined;
}
for (const field of formDefinition.fields) {
if (field.id === fieldId) {
return field;
}
}
return undefined;
}
static getFieldTypeDescription(fieldType) {
switch (fieldType) {
case IField_1.FieldDataType.int:
return "Integer number";
case IField_1.FieldDataType.boolean:
return "Boolean true/false";
case IField_1.FieldDataType.float:
return "Decimal number";
case IField_1.FieldDataType.stringEnum:
return "String from a list of choices";
case IField_1.FieldDataType.intEnum:
return "Integer number from a list of choices";
case IField_1.FieldDataType.intBoolean:
return "Boolean 0/1";
case IField_1.FieldDataType.number:
return "Decimal number";
case IField_1.FieldDataType.long:
return "Large number";
case IField_1.FieldDataType.stringLookup:
return "String from a list of choices";
case IField_1.FieldDataType.intValueLookup:
return "Integer number from a list of choices";
case IField_1.FieldDataType.point3:
return "x, y, z coordinate array";
case IField_1.FieldDataType.intPoint3:
return "integer x, y, z coordinate array";
case IField_1.FieldDataType.longFormString:
return "Longer descriptive text";
case IField_1.FieldDataType.keyedObjectCollection:
return "Named set of objects";
case IField_1.FieldDataType.objectArray:
return "Array of objects";
case IField_1.FieldDataType.object:
return "Object";
case IField_1.FieldDataType.stringArray:
return "Array of strings";
case IField_1.FieldDataType.intRange:
return "Range of integers";
case IField_1.FieldDataType.floatRange:
return "Range of floats";
case IField_1.FieldDataType.minecraftFilter:
return "Minecraft filter";
case IField_1.FieldDataType.minecraftEventTriggerArray:
return "Array of Minecraft Event Triggers";
case IField_1.FieldDataType.percentRange:
return "Percent Range";
case IField_1.FieldDataType.minecraftEventTrigger:
return "Minecraft Event Trigger";
case IField_1.FieldDataType.minecraftEventReference:
return "Minecraft Event Reference";
case IField_1.FieldDataType.longFormStringArray:
return "Array of longer descriptive text";
case IField_1.FieldDataType.keyedStringCollection:
return "Keyed set of strings";
case IField_1.FieldDataType.version:
return "Version";
case IField_1.FieldDataType.uuid:
return "Unique Id";
case IField_1.FieldDataType.keyedBooleanCollection:
return "Keyed collection of boolean values";
case IField_1.FieldDataType.keyedStringArrayCollection:
return "Keyed collection of string arrays";
case IField_1.FieldDataType.arrayOfKeyedStringCollection:
return "Array of keyed string sets";
case IField_1.FieldDataType.keyedKeyedStringArrayCollection:
return "Keyed set of keyed string sets";
case IField_1.FieldDataType.keyedNumberCollection:
return "Keyed set of numbers";
case IField_1.FieldDataType.numberArray:
return "Array of numbers";
case IField_1.FieldDataType.point2:
return "a, b coordinate array";
case IField_1.FieldDataType.localizableString:
return "Localizable String";
case IField_1.FieldDataType.string:
return "String";
case IField_1.FieldDataType.molang:
return "Molang";
case IField_1.FieldDataType.molangArray:
return "Molang array";
default:
return "String";
}
}
/**
* Generate a natural language summary of an object based on its form definition.
*
* This method loads the summarizer associated with the form (if one exists)
* and evaluates it against the provided data to produce human-readable phrases.
*
* @param data The data object to summarize
* @param formPath Path to the form definition (e.g., "entity/minecraft_health")
* @param options Optional evaluation options
* @returns Result containing phrases and formatted output
*
* @example
* const result = await DataFormUtilities.generateSummary(
* { max: 500, value: 500 },
* "entity/minecraft_health"
* );
* console.log(result.asCompleteSentence);
* // "This entity has god-tier health (500 HP)."
*/
static async generateSummary(data, formPath, options) {
// Load the form definition
const form = await Database_1.default.ensureFormLoadedByPath(formPath);
// Extract category from the path (e.g., "entity" from "entity/minecraft_health.form.json")
let category;
const lastSlash = formPath.lastIndexOf("/");
if (lastSlash > 0) {
category = formPath.substring(0, lastSlash);
}
// Derive the summarizer ID from the form path (same name, different extension)
const summarizerName = formPath.substring(lastSlash + 1).replace(".form.json", "");
// Try to load the summarizer
const summarizer = await DataFormUtilities.loadSummarizerById(summarizerName, category);
if (!summarizer) {
// No summarizer defined, return empty result
return {
phrases: [],
asSentence: "",
asCompleteSentence: "",
};
}
const evaluator = new SummarizerEvaluator_1.default();
return evaluator.evaluate(summarizer, data, form, options);
}
/**
* Generate a summary and format it as a complete sentence.
*
* @param data The data object to summarize
* @param formPath Path to the form definition
* @param prefix Optional prefix for the sentence (default: "This entity ")
* @returns A complete sentence describing the object, or empty string if no summarizer
*
* @example
* const sentence = await DataFormUtilities.generateSummaryAsSentence(
* { max: 100 },
* "entity/minecraft_health",
* "This mob "
* );
* // "This mob has extremely high health, on par with an Iron Golem (100 HP)."
*/
static async generateSummaryAsSentence(data, formPath, prefix = "This entity ") {
const result = await DataFormUtilities.generateSummary(data, formPath);
if (result.phrases.length === 0) {
return "";
}
return `${prefix}${result.asSentence}.`;
}
/**
* Load a summarizer definition by ID.
*
* The ID format matches subFormId: "category/name" or just "name".
* Does NOT include the .summarizer.json suffix.
*
* @param summarizerId ID of the summarizer (e.g., "entity/minecraft_health" or "minecraft_health")
* @param category Optional category subfolder (e.g., "entity", "block") - used if ID doesn't include category
* @returns The summarizer definition, or undefined if not found
*
* @example
* // These are equivalent:
* loadSummarizerById("entity/minecraft_health")
* loadSummarizerById("minecraft_health", "entity")
*/
static async loadSummarizerById(summarizerId, category) {
try {
// Parse the ID to extract category and name
const lastSlash = summarizerId.lastIndexOf("/");
let resolvedCategory;
let name;
if (lastSlash >= 0) {
// ID includes category: "entity/minecraft_health"
resolvedCategory = summarizerId.substring(0, lastSlash);
name = summarizerId.substring(lastSlash + 1);
}
else {
// ID is just name: "minecraft_health"
resolvedCategory = category;
name = summarizerId;
}
// Build the full path - use contentWebRoot for Electron compatibility
let relativePath = "data/forms/";
if (resolvedCategory) {
relativePath += resolvedCategory + "/";
}
relativePath += name + ".summarizer.json";
const fullPath = CreatorToolsHost_1.default.contentWebRoot + relativePath;
// Load the summarizer JSON
const response = await fetch(fullPath);
if (!response.ok) {
return undefined;
}
const data = await response.json();
if (!data || typeof data !== "object" || !Array.isArray(data.phrases)) {
return undefined;
}
return data;
}
catch (e) {
// Summarizer not found or invalid
return undefined;
}
}
/**
* Evaluate a summarizer directly against data.
*
* Use this when you already have the summarizer loaded and want to
* avoid the overhead of loading it from disk.
*
* @param summarizer The summarizer definition
* @param data The data object to summarize
* @param form Optional form definition for sample lookup
* @param options Optional evaluation options
* @returns Result containing phrases and formatted output
*/
static evaluateSummarizer(summarizer, data, form, options) {
const evaluator = new SummarizerEvaluator_1.default();
return evaluator.evaluate(summarizer, data, form, options);
}
// ========================================================================
// FIELD PROPERTY UTILITIES
// These are shared utilities for inspecting field structures, used by
// JSON schema generators, hover providers, and other field-analysis code.
// ========================================================================
/**
* Get a preview of property names for an object field.
* Returns a string like "{ description, components, events, ... }" for objects with many properties,
* or "{ prop1, prop2 }" for objects with few properties.
*
* This is useful for displaying concise type information in hovers and tooltips.
*
* @param field The field to inspect
* @returns A formatted string preview of property names, or null if not applicable
*/
static getObjectPropertyPreview(field) {
let fieldNames = [];
// Helper to clean field IDs - removes embedded enum syntax like `render_distance_type"<"fixed", "render"`
const cleanFieldId = (id) => {
const lessThanIndex = id.indexOf('"<"');
if (lessThanIndex > 0) {
return id.substring(0, lessThanIndex);
}
return id;
};
// Check subFields (direct field definitions)
if (field.subFields) {
fieldNames = Object.keys(field.subFields)
.filter((k) => !k.startsWith("_"))
.map(cleanFieldId);
}
// Check subForm (embedded form definition)
else if (field.subForm?.fields) {
fieldNames = field.subForm.fields.filter((f) => f.id && !f.id.startsWith("_")).map((f) => cleanFieldId(f.id));
}
if (fieldNames.length === 0) {
return null;
}
// Show up to 3 property names with ellipsis if more
const maxToShow = 3;
if (fieldNames.length <= maxToShow) {
return `{ ${fieldNames.join(", ")} }`;
}
const shown = fieldNames.slice(0, maxToShow).join(", ");
return `{ ${shown}, ... }`;
}
/**
* Get the child properties of a field (what this field contains).
* Returns the field's own subForm/subFields, or the referenced subFormId's fields.
* Returns null if the field doesn't have child properties.
*
* This supports multiple resolution strategies:
* 1. subFormId reference - looks up in the provided forms dictionary
* 2. Inline subForm - embedded form definition
* 3. subFields - dictionary-style field definitions
*
* @param field The field to inspect
* @param formsBySubFormId Optional dictionary of forms keyed by subFormId for resolving references
* @returns Array of child fields, or null if none found
*/
static getFieldChildProperties(field, formsBySubFormId) {
// First check if field references a subFormId
if (field.subFormId && formsBySubFormId && formsBySubFormId[field.subFormId]) {
const referencedForm = formsBySubFormId[field.subFormId];
if (referencedForm.fields && referencedForm.fields.length > 0) {
return referencedForm.fields;
}
}
// Check inline subForm
if (field.subForm?.fields && field.subForm.fields.length > 0) {
return field.subForm.fields;
}
// Check subFields (dictionary style)
if (field.subFields) {
const subFieldsArray = [];
for (const [key, subField] of Object.entries(field.subFields)) {
if (!key.startsWith("_")) {
subFieldsArray.push({ ...subField, id: key });
}
}
if (subFieldsArray.length > 0) {
return subFieldsArray;
}
}
return null;
}
}
exports.default = DataFormUtilities;