Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/pluggable-widgets-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- We added support for [System texts](https://docs.mendix.com/refguide/system-texts/), introduced to Pluggable Widgets in Mendix 11.13. Texts [defined in the Widget's XML file](https://docs.mendix.com/apidocs-mxsdk/apidocs/pluggable-widgets-property-types/#text) are included in Studio Pro's System text editor. Texts can be defined with parameters and default translations or can be a reference to an external System text.

## [11.12.0] - 2026-06-30

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggable-widgets-tools/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mendix/pluggable-widgets-tools",
"version": "11.12.0",
"version": "11.13.0",
"description": "Mendix Pluggable Widgets Tools",
"engines": {
"node": ">=20"
Expand Down
2 changes: 1 addition & 1 deletion packages/pluggable-widgets-tools/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function ensure<T>(arg?: T, label: string = "argument"): T {
export function ensure<T>(arg: T | undefined | null, label: string = "argument"): T {
if (arg === null || arg === undefined) {
throw new Error(`Did not expect ${label} to be ${arg}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,56 @@ export interface SystemProperty {
};
}

export interface TextSystemProperty {
$: {
key: "Text";
};
text: SystemText[];
externalTexts: ExternalSystemTextNamespace[];
}

export interface SystemText {
$: {
key: string;
};
caption: string;
translations: SystemTextTranslationCollection[];
parameters: SystemTextParameterCollection[];
}

export interface SystemTextParameterCollection {
parameter: SystemTextParameter[];
}

export interface SystemTextTranslationCollection {
translation: SystemTextTranslation[];
}

export interface SystemTextTranslation {
$: {
lang: string;
};
}

export interface SystemTextParameter {
$: {
caption: string;
};
}

export interface ExternalSystemTextNamespace {
$: {
widgetId: string;
};
text: ExternalSystemText[];
}

export interface ExternalSystemText {
$: {
key: string;
};
}

export interface Enumeration {
enumerationValue: EnumerationValue[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {
singleObjectDatasourceNativeOutput,
singleObjectDatasourceWebOutput
} from "./outputs/single-object-datasource";
import { systemTextsInput, systemTextsInputNative } from "./inputs/systemtexts";
import { systemTextsNativeOutput, systemTextsOutput } from "./outputs/systemtexts";

describe("Generating tests", () => {
it("Generates a parsed typing from XML for native", () => {
Expand Down Expand Up @@ -266,6 +268,16 @@ describe("Generating tests", () => {
const newContent = generateNativeTypesFor(singleObjectDatasourceInputNative);
expect(newContent).toBe(singleObjectDatasourceNativeOutput);
});

it("Generates a parsed typing from XML for web using system texts", () => {
const newContent = generateFullTypesFor(systemTextsInput);
expect(newContent).toBe(systemTextsOutput);
});

it("Generates a parsed typing from XML for native using system texts", () => {
const newContent = generateNativeTypesFor(systemTextsInputNative);
expect(newContent).toBe(systemTextsNativeOutput);
});
});

function generateFullTypesFor(xml: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const systemProperty = `<systemProperty key="Text">
<text key="photo">
<caption>Photo</caption>
<translations>
<translation lang="en_US">Photo</translation>
<translation lang="nl_NL">Foto</translation>
</translations>
</text>
<text key="photo_count">
<caption>Photo Count</caption>
<translations>
<translation lang="en_US">{1} Photos</translation>
<translation lang="nl_NL">{1} Foto's</translation>
</translations>
<parameters>
<parameter caption="Count" />
</parameters>
</text>
<externalTexts widgetId="mendix.textbox.TextBox">
<text key="label" />
<text key="placeholder" />
</externalTexts>
<externalTexts widgetId="mendix.datagrid.DataGrid" />
</systemProperty>`;

export const systemTextsInput = `<?xml version="1.0" encoding="utf-8"?>
<widget id="mendix.mywidget.MyWidget" needsEntityContext="true" offlineCapable="true" pluginWidget="true"
xmlns="http://www.mendix.com/widget/1.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../xsd/widget.xsd">
<properties>
<propertyGroup caption="General">
${systemProperty}
</propertyGroup>
</properties>
</widget>`;

export const systemTextsInputNative = `<?xml version="1.0" encoding="utf-8"?>
<widget id="mendix.mywidget.MyWidget" needsEntityContext="true" offlineCapable="true" pluginWidget="true" supportedPlatform="Native"
xmlns="http://www.mendix.com/widget/1.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../xsd/widget.xsd">
<properties>
<propertyGroup caption="General">
${systemProperty}
</propertyGroup>
</properties>
</widget>`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const systemTextsOutput = `/**
* This file was generated from MyWidget.xml
* WARNING: All changes made to this file will be overwritten
* @author Mendix Widgets Framework Team
*/
import { CSSProperties } from "react";

export interface Translations {
"photo": [];
"photo_count": [params: [count: string]];
"mendix.textbox.TextBox": [key: "label" | "placeholder", params?: string[]]
"mendix.datagrid.DataGrid": [key: string, params?: string[]]
}

export interface MyWidgetContainerProps {
name: string;
class: string;
style?: CSSProperties;
tabIndex?: number;
texts: {
translate: <K extends keyof Translations>(key: K, ...params: Translations[K]) => string
}
}

export interface MyWidgetPreviewProps {
/**
* @deprecated Deprecated since version 9.18.0. Please use class property instead.
*/
className: string;
class: string;
style: string;
styleObject?: CSSProperties;
readOnly: boolean;
renderMode: "design" | "xray" | "structure";
translate: (text: string) => string;
}
`;

export const systemTextsNativeOutput = `export interface MyWidgetProps<Style> {
name: string;
style: Style[];
texts: {
translate: <K extends keyof Translations>(key: K, ...params: Translations[K]) => string
}
}`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { generateClientTypes } from "./generateClientTypes";
import { generateImports, ImportableModule } from "./generateImports";
import { generatePreviewTypes } from "./generatePreviewTypes";
import { generateTranslations } from "./generateSystemTexts";
import { extractProperties, extractSystemProperties } from "./helpers";
import { WidgetXml } from "./WidgetXml";

Expand Down Expand Up @@ -52,13 +53,15 @@ export function generateForWidget(widgetXml: WidgetXml, widgetName: string) {
const properties = extractProperties(propElements).filter(prop => prop?.$?.key);
const systemProperties = extractSystemProperties(propElements).filter(prop => prop?.$?.key);

const translations = generateTranslations(systemProperties);
const clientTypes = generateClientTypes(widgetName, properties, systemProperties, isNative);
const modelerTypes = generatePreviewTypes(widgetName, properties, systemProperties);

const generatedTypesCode = clientTypes
.slice(0, clientTypes.length - 1) // all client auxiliary types
.concat(modelerTypes.slice(0, modelerTypes.length - 1)) // all preview auxiliary types
.concat([clientTypes[clientTypes.length - 1], modelerTypes[modelerTypes.length - 1]])
.concat([translations, clientTypes[clientTypes.length - 1], modelerTypes[modelerTypes.length - 1]])
.filter(t => t !== "")
.join("\n\n");

const imports = generateImports(importableModules, generatedTypesCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ActionVariableTypes, Property, ReturnType, SystemProperty } from "./WidgetXml";
import { capitalizeFirstLetter, commasAnd, extractProperties } from "./helpers";
import { systemTextsProperty } from "./generateSystemTexts";
import { capitalizeFirstLetter, commasAnd, extractProperties, templateInterface } from "./helpers";

export function generateClientTypes(
widgetName: string,
Expand All @@ -12,34 +13,40 @@ export function generateClientTypes(
}

const isLabeled = systemProperties.some(p => p.$.key === "Label");
const hasTranslations = systemProperties.some(p => p.$.key === "Text");
const results = Array.of<string>();
const propertyTypes = generateClientTypeProperties(properties, isNative, results, resolveProp);
results.push(
isNative
? `export interface ${widgetName}Props<Style> {
name: string;
style: Style[];
${generateClientTypeBody(properties, true, results, resolveProp)}
}`
: `export interface ${widgetName}ContainerProps {
name: string;${
!isLabeled
? `
class: string;
style?: CSSProperties;`
: ""
}
tabIndex?: number;${
isLabeled
? `
id: string;`
: ""
}
${generateClientTypeBody(properties, false, results, resolveProp)}
}`
? generateNativeProps(widgetName, hasTranslations, propertyTypes)
: generateWebProps(widgetName, isLabeled, hasTranslations, propertyTypes)
);
return results;
}

function generateWebProps(widgetName: string, isLabeled: boolean, hasTranslations: boolean, propertyTypes: string[]) {
return templateInterface(
`${widgetName}ContainerProps`,
"name: string;",
!isLabeled ? `class: string;` : "",
!isLabeled ? `style?: CSSProperties;` : "",
`tabIndex?: number;`,
isLabeled ? `id: string;` : "",
hasTranslations ? systemTextsProperty : "",
...propertyTypes
);
}

function generateNativeProps(widgetName: string, hasTranslations: boolean, propertyTypes: string[]) {
return templateInterface(
`${widgetName}Props<Style>`,
"name: string;",
"style: Style[];",
hasTranslations ? systemTextsProperty : "",
...propertyTypes
);
}

function isEmbeddedOnChangeAction(propertyPath: string, properties: Property[]): boolean {
return properties.some(prop => {
if (prop.$.onChange === propertyPath) {
Expand All @@ -54,12 +61,12 @@ function isEmbeddedOnChangeAction(propertyPath: string, properties: Property[]):
});
}

function generateClientTypeBody(
function generateClientTypeProperties(
properties: Property[],
isNative: boolean,
generatedTypes: string[],
resolveProp: (key: string) => Property | undefined
) {
): string[] {
return properties
.filter(prop => {
if (prop.$.type === "action" && isEmbeddedOnChangeAction(prop.$.key, properties)) {
Expand All @@ -70,16 +77,11 @@ function generateClientTypeBody(
}
return true;
})
.map(
prop =>
` ${prop.$.key}${isOptionalProp(prop, resolveProp) ? "?" : ""}: ${toClientPropType(
prop,
isNative,
generatedTypes,
resolveProp
)};`
)
.join("\n");
.map(prop => {
const optional = isOptionalProp(prop, resolveProp) ? "?" : "";
const type = toClientPropType(prop, isNative, generatedTypes, resolveProp);
return `${prop.$.key}${optional}: ${type};`;
});
}

function isOptionalProp(prop: Property, resolveProp: (key: string) => Property | undefined) {
Expand Down Expand Up @@ -216,9 +218,10 @@ function toClientPropType(
key.startsWith("../") ? resolveProp(key.substring(3)) : childProperties.find(p => p.$.key === key);

generatedTypes.push(
`export interface ${childType} {
${generateClientTypeBody(childProperties, isNative, generatedTypes, resolveChildProp)}
}`
templateInterface(
childType,
...generateClientTypeProperties(childProperties, isNative, generatedTypes, resolveChildProp)
)
);
return prop.$.isList === "true" ? `${childType}[]` : childType;
case "widgets":
Expand Down
Loading
Loading