From e8fddad9b11e9c3efa7b8f4464d7eb88c3269320 Mon Sep 17 00:00:00 2001 From: "Ali.Haydar.Sumer" Date: Wed, 22 Apr 2026 15:28:52 +0200 Subject: [PATCH 1/3] Generate types for single object data sources --- .../src/typings-generator/generate.ts | 1 + .../typings-generator/generateClientTypes.ts | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/pluggable-widgets-tools/src/typings-generator/generate.ts b/packages/pluggable-widgets-tools/src/typings-generator/generate.ts index 9e5fcfb0..3f1f1ce4 100644 --- a/packages/pluggable-widgets-tools/src/typings-generator/generate.ts +++ b/packages/pluggable-widgets-tools/src/typings-generator/generate.ts @@ -25,6 +25,7 @@ const importableModules = [ "ListWidgetValue", "NativeIcon", "NativeImage", + "ObjectItem", "Option", "ReferenceSetValue", "ReferenceValue", diff --git a/packages/pluggable-widgets-tools/src/typings-generator/generateClientTypes.ts b/packages/pluggable-widgets-tools/src/typings-generator/generateClientTypes.ts index d776a176..eb809caf 100644 --- a/packages/pluggable-widgets-tools/src/typings-generator/generateClientTypes.ts +++ b/packages/pluggable-widgets-tools/src/typings-generator/generateClientTypes.ts @@ -100,6 +100,10 @@ export function hasOptionalDataSource(prop: Property, resolveProp: (key: string) return prop.$.dataSource && resolveProp(prop.$.dataSource)?.$.required === "false"; } +function isLinkedToListDataSource(prop: Property, resolveProp: (key: string) => Property | undefined): boolean { + return prop.$.dataSource != null && resolveProp(prop.$.dataSource)?.$.isList === "true"; +} + function toActionVariablesOutputType(actionVariables?: ActionVariableTypes[]) { const types = actionVariables?.flatMap(av => av.actionVariable) .map(avt => `${avt.$.key}: ${toOption(toAttributeClientType(avt.$.type))}`) @@ -121,9 +125,9 @@ function toClientPropType( return "string"; case "action": const variableTypes = toActionVariablesOutputType(prop.actionVariables); - return (prop.$.dataSource ? "ListActionValue" : "ActionValue") + variableTypes; + return (isLinkedToListDataSource(prop, resolveProp) ? "ListActionValue" : "ActionValue") + variableTypes; case "textTemplate": - return prop.$.dataSource ? "ListExpressionValue" : "DynamicValue"; + return isLinkedToListDataSource(prop, resolveProp) ? "ListExpressionValue" : "DynamicValue"; case "integer": return "number"; case "decimal": @@ -135,7 +139,7 @@ function toClientPropType( case "file": return prop.$.allowUpload ? "EditableFileValue" : "DynamicValue"; case "datasource": - return "ListValue"; + return prop.$.isList === "true" ? "ListValue" : "DynamicValue"; case "attribute": { if (!prop.attributeTypes?.length) { throw new Error("[XML] Attribute property requires attributeTypes element"); @@ -144,22 +148,22 @@ function toClientPropType( .flatMap(ats => ats.attributeType) .map(at => toAttributeClientType(at.$.name)); const unionType = toUniqueUnionType(types); - const linkedToDataSource = !!prop.$.dataSource; + const linkedToListDS = isLinkedToListDataSource(prop, resolveProp); if (prop.$.isMetaData === "true") { - if (!linkedToDataSource) { + if (!prop.$.dataSource) { throw new Error(`[XML] Attribute property can only have isMetaData="true" when linked to a datasource`); } return `AttributeMetaData<${unionType}>`; } if (!prop.associationTypes?.length) { - return toAttributeOutputType("Reference", linkedToDataSource, unionType); + return toAttributeOutputType("Reference", linkedToListDS, unionType); } else { const reftypes = prop.associationTypes .flatMap(ats => ats.associationType) - .map(at => toAttributeOutputType(at.$.name, linkedToDataSource, unionType)); + .map(at => toAttributeOutputType(at.$.name, linkedToListDS, unionType)); return toUniqueUnionType(reftypes); } } @@ -168,9 +172,9 @@ function toClientPropType( throw new Error("[XML] Association property requires associationTypes element"); } - const linkedToDataSource = !!prop.$.dataSource; + const linkedToListDS = isLinkedToListDataSource(prop, resolveProp); if (prop.$.isMetaData === "true") { - if (!linkedToDataSource) { + if (!prop.$.dataSource) { throw new Error(`[XML] Association property can only have isMetaData="true" when linked to a datasource`); } return "AssociationMetaData"; @@ -178,7 +182,7 @@ function toClientPropType( const types = prop.associationTypes .flatMap(ats => ats.associationType) - .map(at => toAssociationOutputType(at.$.name, linkedToDataSource)); + .map(at => toAssociationOutputType(at.$.name, linkedToListDS)); return toUniqueUnionType(types); } case "expression": @@ -186,7 +190,7 @@ function toClientPropType( throw new Error("[XML] Expression property requires returnType element"); } const type = toExpressionClientType(prop.returnType[0], resolveProp); - return prop.$.dataSource ? `ListExpressionValue<${type}>` : `DynamicValue<${type}>`; + return isLinkedToListDataSource(prop, resolveProp) ? `ListExpressionValue<${type}>` : `DynamicValue<${type}>`; case "enumeration": const typeName = capitalizeFirstLetter(prop.$.key) + "Enum"; generatedTypes.push(generateEnum(typeName, prop)); @@ -208,7 +212,7 @@ ${generateClientTypeBody(childProperties, isNative, generatedTypes, resolveChild ); return prop.$.isList === "true" ? `${childType}[]` : childType; case "widgets": - return prop.$.dataSource ? "ListWidgetValue" : "ReactNode"; + return isLinkedToListDataSource(prop, resolveProp) ? "ListWidgetValue" : "ReactNode"; case "selection": if (!prop.selectionTypes?.length) { throw new Error("[XML] Selection property requires selectionTypes element"); From 3615687d8145a0df4757289a8e2e2c494523a69e Mon Sep 17 00:00:00 2001 From: "Ali.Haydar.Sumer" Date: Wed, 22 Apr 2026 15:29:08 +0200 Subject: [PATCH 2/3] Add unit tests --- .../typings-generator/__tests__/index.spec.ts | 12 ++ .../inputs/single-object-datasource.ts | 136 ++++++++++++++++++ .../outputs/single-object-datasource.ts | 62 ++++++++ 3 files changed, 210 insertions(+) create mode 100644 packages/pluggable-widgets-tools/src/typings-generator/__tests__/inputs/single-object-datasource.ts create mode 100644 packages/pluggable-widgets-tools/src/typings-generator/__tests__/outputs/single-object-datasource.ts diff --git a/packages/pluggable-widgets-tools/src/typings-generator/__tests__/index.spec.ts b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/index.spec.ts index 50c894d8..0d6793a5 100644 --- a/packages/pluggable-widgets-tools/src/typings-generator/__tests__/index.spec.ts +++ b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/index.spec.ts @@ -47,6 +47,8 @@ import {listActionWithVariablesInput, listActionWithVariablesInputNative} from " import {listActionWithVariablesOutput, listActionWithVariablesOutputNative} from "./outputs/list-action-with-variables"; import {imageWebInput, imageNativeInput} from "./inputs/image"; import {imageWebOutput, imageNativeOutput} from "./outputs/image"; +import { singleObjectDatasourceInput, singleObjectDatasourceInputNative } from "./inputs/single-object-datasource"; +import { singleObjectDatasourceNativeOutput, singleObjectDatasourceWebOutput } from "./outputs/single-object-datasource"; describe("Generating tests", () => { it("Generates a parsed typing from XML for native", () => { @@ -248,6 +250,16 @@ describe("Generating tests", () => { const newContent = generateNativeTypesFor(imageNativeInput); expect(newContent).toBe(imageNativeOutput); }); + + it("Generates a parsed typing from XML for web using single object datasource", () => { + const newContent = generateFullTypesFor(singleObjectDatasourceInput); + expect(newContent).toBe(singleObjectDatasourceWebOutput); + }); + + it("Generates a parsed typing from XML for native using single object datasource", () => { + const newContent = generateNativeTypesFor(singleObjectDatasourceInputNative); + expect(newContent).toBe(singleObjectDatasourceNativeOutput); + }); }); function generateFullTypesFor(xml: string) { diff --git a/packages/pluggable-widgets-tools/src/typings-generator/__tests__/inputs/single-object-datasource.ts b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/inputs/single-object-datasource.ts new file mode 100644 index 00000000..97850552 --- /dev/null +++ b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/inputs/single-object-datasource.ts @@ -0,0 +1,136 @@ +export const singleObjectDatasourceInput = ` + + + + + Single object data source + + + + Optional single object data source + + + + List data source + + + + Single Content + + + + Single Attribute + + + + + + + + + Single Action + + + + Single Text Template + + + + Single Expression + + + + + Optional Single Attribute + + + + + + + Optional Single Action + + + + List Content + + + + List Attribute + + + + + + + List Action + + + + + + + + +`; + +export const singleObjectDatasourceInputNative = ` + + + + + Single object data source + + + + List data source + + + + Single Content + + + + Single Attribute + + + + + + + + + Single Action + + + + Single Text Template + + + + Single Expression + + + + + List Content + + + + List Attribute + + + + + + + List Action + + + + +`; diff --git a/packages/pluggable-widgets-tools/src/typings-generator/__tests__/outputs/single-object-datasource.ts b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/outputs/single-object-datasource.ts new file mode 100644 index 00000000..9aec6809 --- /dev/null +++ b/packages/pluggable-widgets-tools/src/typings-generator/__tests__/outputs/single-object-datasource.ts @@ -0,0 +1,62 @@ +export const singleObjectDatasourceWebOutput = `/** + * This file was generated from MyWidget.xml + * WARNING: All changes made to this file will be overwritten + * @author Mendix Widgets Framework Team + */ +import { ActionValue, DynamicValue, EditableValue, ListActionValue, ListAttributeValue, ListValue, ListWidgetValue, ObjectItem } from "mendix"; +import { ComponentType, ReactNode } from "react"; +import { Big } from "big.js"; + +export interface MyWidgetContainerProps { + name: string; + tabIndex?: number; + id: string; + singleSource: DynamicValue; + optionalSingleSource?: DynamicValue; + listSource: ListValue; + singleContent: ReactNode; + singleAttribute: EditableValue; + singleAction?: ActionValue; + singleTextTemplate: DynamicValue; + singleExpression: DynamicValue; + optionalSingleAttribute?: EditableValue; + optionalSingleAction?: ActionValue; + listContent: ListWidgetValue; + listAttribute: ListAttributeValue; + listAction?: ListActionValue; +} + +export interface MyWidgetPreviewProps { + readOnly: boolean; + renderMode: "design" | "xray" | "structure"; + translate: (text: string) => string; + singleSource: {} | { caption: string } | { type: string } | null; + optionalSingleSource: {} | { caption: string } | { type: string } | null; + listSource: {} | { caption: string } | { type: string } | null; + singleContent: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; + singleAttribute: string; + singleAction: {} | null; + singleTextTemplate: string; + singleExpression: string; + optionalSingleAttribute: string; + optionalSingleAction: {} | null; + listContent: { widgetCount: number; renderer: ComponentType<{ children: ReactNode; caption?: string }> }; + listAttribute: string; + listAction: {} | null; +} +`; + +export const singleObjectDatasourceNativeOutput = `export interface MyWidgetProps