diff --git a/.claude/skills/mendix/custom-widgets.md b/.claude/skills/mendix/custom-widgets.md new file mode 100644 index 0000000..baa1a73 --- /dev/null +++ b/.claude/skills/mendix/custom-widgets.md @@ -0,0 +1,337 @@ +--- +name: mendix-custom-widgets +description: Use when writing MDL for GALLERY, COMBOBOX, or third-party pluggable widgets in CREATE PAGE / ALTER PAGE statements. Covers built-in widget syntax, child slots (TEMPLATE/FILTER), adding new custom widgets via .def.json, and engine internals. +--- + +# Custom & Pluggable Widgets in MDL + +## Built-in Pluggable Widgets + +### GALLERY + +Card-layout list with optional template content and filters. + +```sql +GALLERY galleryName ( + DataSource: DATABASE FROM Module.Entity SORT BY Name ASC, + Selection: Single | Multiple | None +) { + TEMPLATE template1 { + DYNAMICTEXT title (Content: '{1}', ContentParams: [{1} = Name], RenderMode: H4) + DYNAMICTEXT info (Content: '{1}', ContentParams: [{1} = Email]) + } + FILTER filter1 { + TEXTFILTER searchName (Attribute: Name) + NUMBERFILTER searchScore (Attribute: Score) + DROPDOWNFILTER searchStatus (Attribute: Status) + DATEFILTER searchDate (Attribute: CreatedAt) + } +} +``` + +- `TEMPLATE` block -> mapped to `content` property (child widgets rendered per row) +- `FILTER` block -> mapped to `filtersPlaceholder` property (shown above list) +- `Selection: None` omits the selection property (default if omitted) +- Children written directly under GALLERY (no container) go to the first slot with `mdlContainer: "TEMPLATE"` + +### COMBOBOX + +Two modes depending on the attribute type: + +```sql +-- Enumeration mode (Attribute is an enum) +COMBOBOX cbStatus (Label: 'Status', Attribute: Status) + +-- Association mode (Attribute is an association) +COMBOBOX cmbCustomer ( + Label: 'Customer', + Attribute: Order_Customer, + DataSource: DATABASE Module.Customer, + CaptionAttribute: Name +) +``` + +- Engine detects association mode when `DataSource` is present (`hasDataSource` condition) +- `CaptionAttribute` is the display attribute on the **target** entity +- In association mode, mapping order matters: DataSource must resolve before Association (sets entityContext) + +## Adding a Third-Party Widget + +### Step 1 -- Extract .def.json from .mpk + +```bash +mxcli widget extract --mpk widgets/MyWidget.mpk +# Output: .mxcli/widgets/mywidget.def.json + +# Override MDL keyword +mxcli widget extract --mpk widgets/MyWidget.mpk --mdl-name MYWIDGET +``` + +The `extract` command parses the .mpk (ZIP archive containing `package.xml` + widget XML) and auto-infers operations from XML property types: + +| XML Type | Operation | MDL Source Key | +|----------|-----------|----------------| +| attribute | attribute | `Attribute` | +| association | association | `Association` | +| datasource | datasource | `DataSource` | +| selection | selection | `Selection` | +| widgets | widgets (child slot) | container name (key uppercased) | +| boolean/string/enumeration/integer/decimal | primitive | hardcoded `Value` from defaultValue | +| action/expression/textTemplate/object/icon/image/file | *skipped* | too complex for auto-mapping | + +Skipped types require manual configuration in the .def.json. + +### Step 2 -- Extract BSON template from Studio Pro + +The .def.json only describes mapping rules. The engine also needs a **template JSON** with the complete Type + Object BSON structure. + +```bash +# 1. In Studio Pro: drag the widget onto a test page, save the project +# 2. Extract the widget's BSON: +mxcli bson dump -p App.mpr --type page --object "Module.TestPage" --format json +# 3. Extract the Type and Object fields from the CustomWidget, save as: +``` + +Place at: `project/.mxcli/widgets/mywidget.json` + +Template JSON format: + +```json +{ + "widgetId": "com.vendor.widget.MyWidget", + "name": "My Widget", + "version": "1.0.0", + "extractedFrom": "TestModule.TestPage", + "type": { + "$ID": "aa000000000000000000000000000001", + "$Type": "CustomWidgets$CustomWidgetType", + "WidgetId": "com.vendor.widget.MyWidget", + "PropertyTypes": [ + { + "$ID": "aa000000000000000000000000000010", + "$Type": "CustomWidgets$WidgetPropertyType", + "PropertyKey": "datasource", + "ValueType": { "$ID": "...", "Type": "DataSource" } + } + ] + }, + "object": { + "$ID": "aa000000000000000000000000000100", + "$Type": "CustomWidgets$WidgetObject", + "TypePointer": "aa000000000000000000000000000001", + "Properties": [ + 2, + { + "$ID": "...", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "aa000000000000000000000000000010", + "Value": { + "$Type": "CustomWidgets$WidgetValue", + "DataSource": null, + "AttributeRef": null, + "PrimitiveValue": "", + "Widgets": [2], + "Selection": "None" + } + } + ] + } +} +``` + +**CRITICAL**: Template must include both `type` (PropertyTypes schema) and `object` (default WidgetObject with all property values). Extract from a real Studio Pro MPR -- do NOT generate programmatically. Mismatched structure causes CE0463. + +### Step 3 -- Place files + +``` +project/.mxcli/widgets/mywidget.def.json <- project scope (highest priority) +project/.mxcli/widgets/mywidget.json <- template JSON (same directory) +~/.mxcli/widgets/mywidget.def.json <- global scope +``` + +Set `"templateFile": "mywidget.json"` in the .def.json. Project definitions override global ones; global overrides embedded. + +### Step 4 -- Use in MDL + +```sql +MYWIDGET myWidget1 (DataSource: DATABASE Module.Entity, Attribute: Name) { + TEMPLATE content1 { + DYNAMICTEXT label1 (Content: '{1}', ContentParams: [{1}=Name]) + } +} +``` + +## .def.json Reference + +```json +{ + "widgetId": "com.vendor.widget.web.mywidget.MyWidget", + "mdlName": "MYWIDGET", + "templateFile": "mywidget.json", + "defaultEditable": "Always", + "propertyMappings": [ + {"propertyKey": "datasource", "source": "DataSource", "operation": "datasource"}, + {"propertyKey": "attribute", "source": "Attribute", "operation": "attribute"}, + {"propertyKey": "someFlag", "value": "true", "operation": "primitive"} + ], + "childSlots": [ + {"propertyKey": "content", "mdlContainer": "TEMPLATE", "operation": "widgets"} + ], + "modes": [ + { + "name": "association", + "condition": "hasDataSource", + "propertyMappings": [ + {"propertyKey": "optionsSource", "value": "association", "operation": "primitive"}, + {"propertyKey": "assocDS", "source": "DataSource", "operation": "datasource"}, + {"propertyKey": "assoc", "source": "Association", "operation": "association"} + ] + }, + { + "name": "default", + "propertyMappings": [ + {"propertyKey": "attr", "source": "Attribute", "operation": "attribute"} + ] + } + ] +} +``` + +### Mode Conditions + +| Condition | Checks | +|-----------|--------| +| `hasDataSource` | AST widget has a `DataSource` property | +| `hasAttribute` | AST widget has an `Attribute` property | +| `hasProp:XYZ` | AST widget has a property named `XYZ` | + +Modes are evaluated in definition order -- first match wins. A mode with no `condition` is the default fallback. + +### 6 Built-in Operations + +| Operation | What it does | Typical Source | +|-----------|-------------|----------------| +| `attribute` | Sets `Value.AttributeRef` on a WidgetProperty | `Attribute` | +| `association` | Sets `Value.AttributeRef` + `Value.EntityRef` | `Association` | +| `primitive` | Sets `Value.PrimitiveValue` | static `value` or property name | +| `datasource` | Sets `Value.DataSource` (serialized BSON) | `DataSource` | +| `selection` | Sets `Value.Selection` (mode string) | `Selection` | +| `widgets` | Replaces `Value.Widgets` array with child widget BSON | child slot | + +### Mapping Order Constraints + +- **`Association` source must come AFTER `DataSource` source** in the mappings array. The association operation depends on `entityContext` set by a prior DataSource mapping. The registry validates this at load time. +- **`value` takes priority over `source`**: if both are set, the static `value` is used. + +### Source Resolution + +| Source | Resolution logic | +|--------|-----------------| +| `Attribute` | `w.GetAttribute()` -> `pageBuilder.resolveAttributePath()` | +| `DataSource` | `w.GetDataSource()` -> `pageBuilder.buildDataSourceV3()` -> also updates `entityContext` | +| `Association` | `w.GetAttribute()` -> `pageBuilder.resolveAssociationPath()` + uses current `entityContext` | +| `Selection` | `w.GetSelection()` or `mapping.Default` fallback | +| `CaptionAttribute` | `w.GetStringProp("CaptionAttribute")` -> auto-prefixed with `entityContext` if relative | +| *(other)* | Treated as generic property name: `w.GetStringProp(source)` | + +## Engine Internals + +### Build Pipeline + +When `buildWidgetV3()` encounters an unrecognized widget type: + +``` +1. Registry lookup: widgetRegistry.Get("MYWIDGET") -> WidgetDefinition +2. Template loading: GetTemplateFullBSON(widgetID, idGenerator, projectPath) + a. Load JSON from embed.FS (or .mxcli/widgets/) + b. Augment from project's .mpk (if newer version available) + c. Phase 1: Collect all $ID values -> generate new UUID mapping + d. Phase 2: Convert Type JSON -> BSON, extract PropertyTypeIDMap + e. Phase 3: Convert Object JSON -> BSON (TypePointer remapped via same mapping) + f. Placeholder leak check (aa000000-prefix IDs must all be remapped) +3. Mode selection: evaluateCondition() on each mode in order -> first match wins +4. Property mappings: for each mapping, resolveMapping() -> OperationFunc() + Each operation locates the WidgetProperty by matching TypePointer against PropertyTypeIDMap +5. Child slots: group AST children by container name, build to BSON, embed via opWidgets +6. Assemble CustomWidget{RawType, RawObject, PropertyTypeIDMap, ObjectTypeID} +``` + +### PropertyTypeIDMap + +The map links PropertyKey names (from .def.json) to their BSON IDs: + +``` +PropertyTypeIDMap["datasource"] = { + PropertyTypeID: "a1b2c3d4...", // $ID of WidgetPropertyType in Type + ValueTypeID: "e5f6a7b8...", // $ID of ValueType within PropertyType + DefaultValue: "", + ValueType: "DataSource", // Type string + ObjectTypeID: "...", // For nested object list properties +} +``` + +Operations use this map to locate the correct WidgetProperty in the Object's Properties array by comparing `TypePointer` (binary GUID) against `PropertyTypeID`. + +### MPK Augmentation + +At template load time, `augmentFromMPK()` checks if the project has a newer `.mpk` for the widget: + +``` +project/widgets/*.mpk -> FindMPK(projectDir, widgetID) -> ParseMPK() +-> AugmentTemplate(clone, mpkDef) + -> Add missing properties from newer .mpk version + -> Remove stale properties no longer in .mpk +``` + +This reduces CE0463 errors from widget version drift without requiring manual template re-extraction. + +### 3-Tier Registry + +| Priority | Location | Scope | +|----------|----------|-------| +| 1 (highest) | `/.mxcli/widgets/*.def.json` | Project | +| 2 | `~/.mxcli/widgets/*.def.json` | Global (user) | +| 3 (lowest) | `sdk/widgets/definitions/*.def.json` (embedded) | Built-in | + +Higher priority definitions override lower ones with the same MDL name (case-insensitive). + +## Verify & Debug + +```bash +# List registered widgets +mxcli widget list -p App.mpr + +# Check after creating a page +mxcli check script.mdl -p App.mpr --references + +# Full mx check (catches CE0463) +~/.mxcli/mxbuild/*/modeler/mx check App.mpr + +# Debug CE0463 -- compare NDSL dumps +mxcli bson dump -p App.mpr --type page --object "Module.PageName" --format ndsl +``` + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| CE0463 after page creation | Template version mismatch -- extract fresh template from Studio Pro MPR, or ensure .mpk augmentation picks up new properties | +| Widget not recognized | Check `mxcli widget list`; .def.json must be in `.mxcli/widgets/` with `.def.json` extension | +| TEMPLATE content missing | Widget needs `childSlots` entry with `"mdlContainer": "TEMPLATE"` | +| Association COMBOBOX shows enum behavior | Add `DataSource` to trigger association mode (`hasDataSource` condition) | +| Association mapping fails | Ensure DataSource mapping appears **before** Association mapping in the array | +| Custom widget not found | Place .def.json in `.mxcli/widgets/` inside the project directory | +| Placeholder ID leak error | Template JSON has unreferenced `$ID` values starting with `aa000000` -- ensure all IDs are in the `collectIDs` traversal path | + +## Key Source Files + +| File | Purpose | +|------|---------| +| `mdl/executor/widget_engine.go` | PluggableWidgetEngine, 6 operations, Build() pipeline | +| `mdl/executor/widget_registry.go` | 3-tier WidgetRegistry, definition validation | +| `sdk/widgets/loader.go` | Template loading, ID remapping, MPK augmentation | +| `sdk/widgets/mpk/mpk.go` | .mpk ZIP parsing, XML property extraction | +| `cmd/mxcli/cmd_widget.go` | `mxcli widget extract/list` CLI commands | +| `sdk/widgets/definitions/*.def.json` | Built-in widget definitions (ComboBox, Gallery) | +| `sdk/widgets/templates/mendix-11.6/*.json` | Embedded BSON templates | +| `mdl/executor/cmd_pages_builder_input.go` | `updateWidgetPropertyValue()` -- TypePointer matching | diff --git a/.mxcli/widgets/gallery.def.json b/.mxcli/widgets/gallery.def.json new file mode 100644 index 0000000..2510aa9 --- /dev/null +++ b/.mxcli/widgets/gallery.def.json @@ -0,0 +1,85 @@ +{ + "widgetId": "com.mendix.widget.web.gallery.Gallery", + "mdlName": "GALLERY", + "templateFile": "gallery.json", + "defaultEditable": "Always", + "propertyMappings": [ + { + "propertyKey": "advanced", + "value": "false", + "operation": "primitive" + }, + { + "propertyKey": "datasource", + "source": "DataSource", + "operation": "datasource" + }, + { + "propertyKey": "itemSelection", + "source": "Selection", + "operation": "selection" + }, + { + "propertyKey": "itemSelectionMode", + "value": "clear", + "operation": "primitive" + }, + { + "propertyKey": "desktopItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "tabletItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "phoneItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "pageSize", + "value": "20", + "operation": "primitive" + }, + { + "propertyKey": "pagination", + "value": "buttons", + "operation": "primitive" + }, + { + "propertyKey": "pagingPosition", + "value": "below", + "operation": "primitive" + }, + { + "propertyKey": "showEmptyPlaceholder", + "value": "none", + "operation": "primitive" + }, + { + "propertyKey": "onClickTrigger", + "value": "single", + "operation": "primitive" + } + ], + "childSlots": [ + { + "propertyKey": "content", + "mdlContainer": "TEMPLATE", + "operation": "widgets" + }, + { + "propertyKey": "emptyPlaceholder", + "mdlContainer": "EMPTYPLACEHOLDER", + "operation": "widgets" + }, + { + "propertyKey": "filtersPlaceholder", + "mdlContainer": "FILTERSPLACEHOLDER", + "operation": "widgets" + } + ] +} diff --git a/cmd/mxcli/cmd_extract_templates.go b/cmd/mxcli/cmd_extract_templates.go index 8743acf..3476276 100644 --- a/cmd/mxcli/cmd_extract_templates.go +++ b/cmd/mxcli/cmd_extract_templates.go @@ -75,6 +75,7 @@ func runExtractTemplates(cmd *cobra.Command, args []string) error { name string }{ {"com.mendix.widget.web.combobox.Combobox", "combobox.json", "Combo box"}, + {"com.mendix.widget.web.gallery.Gallery", "gallery.json", "Gallery"}, {"com.mendix.widget.web.datagrid.Datagrid", "datagrid.json", "Data grid 2"}, {"com.mendix.widget.web.datagridtextfilter.DatagridTextFilter", "datagrid-text-filter.json", "Text filter"}, {"com.mendix.widget.web.datagriddatefilter.DatagridDateFilter", "datagrid-date-filter.json", "Date filter"}, diff --git a/cmd/mxcli/cmd_widget.go b/cmd/mxcli/cmd_widget.go new file mode 100644 index 0000000..a6f4fe1 --- /dev/null +++ b/cmd/mxcli/cmd_widget.go @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/mendixlabs/mxcli/mdl/executor" + "github.com/mendixlabs/mxcli/sdk/widgets/mpk" + "github.com/spf13/cobra" +) + +var widgetCmd = &cobra.Command{ + Use: "widget", + Short: "Widget management commands", +} + +var widgetExtractCmd = &cobra.Command{ + Use: "extract", + Short: "Extract widget definition from an .mpk file", + Long: `Extract a pluggable widget definition from a Mendix .mpk package file +and generate a skeleton .def.json for use with the pluggable widget engine. + +The command parses the widget XML inside the .mpk to discover properties, +infers the appropriate operation for each property based on its type, +and writes the result to the project's .mxcli/widgets/ directory. + +Examples: + mxcli widget extract --mpk widgets/MyWidget.mpk + mxcli widget extract --mpk widgets/MyWidget.mpk --output .mxcli/widgets/ + mxcli widget extract --mpk widgets/MyWidget.mpk --mdl-name MYWIDGET`, + RunE: runWidgetExtract, +} + +var widgetListCmd = &cobra.Command{ + Use: "list", + Short: "List registered widget definitions", + Long: `List all widget definitions available in the pluggable widget engine registry.`, + RunE: runWidgetList, +} + +func init() { + widgetExtractCmd.Flags().String("mpk", "", "Path to .mpk widget package file") + widgetExtractCmd.Flags().StringP("output", "o", "", "Output directory (default: .mxcli/widgets/)") + widgetExtractCmd.Flags().String("mdl-name", "", "Override the MDL keyword name (default: derived from widget name)") + widgetExtractCmd.MarkFlagRequired("mpk") + + widgetCmd.AddCommand(widgetExtractCmd) + widgetCmd.AddCommand(widgetListCmd) + rootCmd.AddCommand(widgetCmd) +} + +func runWidgetExtract(cmd *cobra.Command, args []string) error { + mpkPath, _ := cmd.Flags().GetString("mpk") + outputDir, _ := cmd.Flags().GetString("output") + mdlNameOverride, _ := cmd.Flags().GetString("mdl-name") + + // Parse .mpk + mpkDef, err := mpk.ParseMPK(mpkPath) + if err != nil { + return fmt.Errorf("failed to parse .mpk: %w", err) + } + + // Determine output directory + if outputDir == "" { + outputDir = filepath.Join(".mxcli", "widgets") + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Determine MDL name + mdlName := mdlNameOverride + if mdlName == "" { + mdlName = deriveMDLName(mpkDef.ID) + } + + // Generate .def.json + defJSON := generateDefJSON(mpkDef, mdlName) + + // Determine output filename + filename := strings.ToLower(mdlName) + ".def.json" + outPath := filepath.Join(outputDir, filename) + + data, err := json.MarshalIndent(defJSON, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal definition: %w", err) + } + data = append(data, '\n') + + if err := os.WriteFile(outPath, data, 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", outPath, err) + } + + fmt.Printf("Extracted widget definition:\n") + fmt.Printf(" Widget ID: %s\n", mpkDef.ID) + fmt.Printf(" MDL name: %s\n", mdlName) + fmt.Printf(" Properties: %d\n", len(mpkDef.Properties)) + fmt.Printf(" Output: %s\n", outPath) + + return nil +} + +// deriveMDLName derives an uppercase MDL keyword from a widget ID. +// e.g. "com.mendix.widget.web.combobox.Combobox" → "COMBOBOX" +// e.g. "com.company.widget.MyCustomWidget" → "MYCUSTOMWIDGET" +func deriveMDLName(widgetID string) string { + parts := strings.Split(widgetID, ".") + name := parts[len(parts)-1] + return strings.ToUpper(name) +} + +// generateDefJSON creates a WidgetDefinition from an mpk.WidgetDefinition. +func generateDefJSON(mpkDef *mpk.WidgetDefinition, mdlName string) *executor.WidgetDefinition { + def := &executor.WidgetDefinition{ + WidgetID: mpkDef.ID, + MDLName: mdlName, + TemplateFile: strings.ToLower(mdlName) + ".json", + DefaultEditable: "Always", + } + + // Build property mappings by inferring operations from XML types + var mappings []executor.PropertyMapping + var childSlots []executor.ChildSlotMapping + + for _, prop := range mpkDef.Properties { + normalizedType := mpk.NormalizeType(prop.Type) + + switch normalizedType { + case "attribute": + mappings = append(mappings, executor.PropertyMapping{ + PropertyKey: prop.Key, + Source: "Attribute", + Operation: "attribute", + }) + case "association": + mappings = append(mappings, executor.PropertyMapping{ + PropertyKey: prop.Key, + Source: "Association", + Operation: "association", + }) + case "datasource": + mappings = append(mappings, executor.PropertyMapping{ + PropertyKey: prop.Key, + Source: "DataSource", + Operation: "datasource", + }) + case "widgets": + // Widgets properties become child slots + containerName := strings.ToUpper(prop.Key) + if containerName == "CONTENT" { + containerName = "TEMPLATE" + } + childSlots = append(childSlots, executor.ChildSlotMapping{ + PropertyKey: prop.Key, + MDLContainer: containerName, + Operation: "widgets", + }) + case "selection": + mappings = append(mappings, executor.PropertyMapping{ + PropertyKey: prop.Key, + Source: "Selection", + Operation: "selection", + Default: prop.DefaultValue, + }) + case "boolean", "string", "enumeration", "integer", "decimal": + mapping := executor.PropertyMapping{ + PropertyKey: prop.Key, + Operation: "primitive", + } + if prop.DefaultValue != "" { + mapping.Value = prop.DefaultValue + } + mappings = append(mappings, mapping) + // Skip action, expression, textTemplate, object, icon, image, file — too complex for auto-mapping + } + } + + def.PropertyMappings = mappings + def.ChildSlots = childSlots + + return def +} + +func runWidgetList(cmd *cobra.Command, args []string) error { + registry, err := executor.NewWidgetRegistry() + if err != nil { + return fmt.Errorf("failed to create widget registry: %w", err) + } + + // Load user definitions if project path available + projectPath, _ := cmd.Flags().GetString("project") + if projectPath != "" { + _ = registry.LoadUserDefinitions(projectPath) + } + + defs := registry.All() + if len(defs) == 0 { + fmt.Println("No widget definitions registered.") + return nil + } + + fmt.Printf("%-20s %-50s %s\n", "MDL Name", "Widget ID", "Template") + fmt.Printf("%-20s %-50s %s\n", strings.Repeat("-", 20), strings.Repeat("-", 50), strings.Repeat("-", 20)) + for _, def := range defs { + fmt.Printf("%-20s %-50s %s\n", def.MDLName, def.WidgetID, def.TemplateFile) + } + fmt.Printf("\nTotal: %d definitions\n", len(defs)) + + return nil +} diff --git a/cmd/mxcli/cmd_widget_test.go b/cmd/mxcli/cmd_widget_test.go new file mode 100644 index 0000000..061810e --- /dev/null +++ b/cmd/mxcli/cmd_widget_test.go @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/executor" + "github.com/mendixlabs/mxcli/sdk/widgets/mpk" +) + +func TestDeriveMDLName(t *testing.T) { + tests := []struct { + widgetID string + expected string + }{ + {"com.mendix.widget.web.combobox.Combobox", "COMBOBOX"}, + {"com.mendix.widget.web.gallery.Gallery", "GALLERY"}, + {"com.company.widget.MyCustomWidget", "MYCUSTOMWIDGET"}, + {"SimpleWidget", "SIMPLEWIDGET"}, + } + + for _, tc := range tests { + t.Run(tc.widgetID, func(t *testing.T) { + result := deriveMDLName(tc.widgetID) + if result != tc.expected { + t.Errorf("deriveMDLName(%q) = %q, want %q", tc.widgetID, result, tc.expected) + } + }) + } +} + +func TestGenerateDefJSON(t *testing.T) { + mpkDef := &mpk.WidgetDefinition{ + ID: "com.example.widget.TestWidget", + Name: "Test Widget", + Properties: []mpk.PropertyDef{ + {Key: "datasource", Type: "datasource"}, + {Key: "content", Type: "widgets"}, + {Key: "filterBar", Type: "widgets"}, + {Key: "myAttribute", Type: "attribute"}, + {Key: "showHeader", Type: "boolean", DefaultValue: "true"}, + {Key: "itemSelection", Type: "selection", DefaultValue: "Single"}, + {Key: "myAssociation", Type: "association"}, + {Key: "pageSize", Type: "integer", DefaultValue: "10"}, + }, + } + + def := generateDefJSON(mpkDef, "TESTWIDGET") + + // Verify basic fields + if def.WidgetID != "com.example.widget.TestWidget" { + t.Errorf("WidgetID = %q, want %q", def.WidgetID, "com.example.widget.TestWidget") + } + if def.MDLName != "TESTWIDGET" { + t.Errorf("MDLName = %q, want %q", def.MDLName, "TESTWIDGET") + } + if def.TemplateFile != "testwidget.json" { + t.Errorf("TemplateFile = %q, want %q", def.TemplateFile, "testwidget.json") + } + if def.DefaultEditable != "Always" { + t.Errorf("DefaultEditable = %q, want %q", def.DefaultEditable, "Always") + } + + // Verify property mappings count (datasource, attribute, boolean, selection, association, integer = 6) + if len(def.PropertyMappings) != 6 { + t.Fatalf("PropertyMappings count = %d, want 6", len(def.PropertyMappings)) + } + + // Verify child slots (content → TEMPLATE, filterBar → FILTERBAR) + if len(def.ChildSlots) != 2 { + t.Fatalf("ChildSlots count = %d, want 2", len(def.ChildSlots)) + } + + // content → TEMPLATE (special case) + if def.ChildSlots[0].MDLContainer != "TEMPLATE" { + t.Errorf("ChildSlots[0].MDLContainer = %q, want %q", def.ChildSlots[0].MDLContainer, "TEMPLATE") + } + // filterBar → FILTERBAR + if def.ChildSlots[1].MDLContainer != "FILTERBAR" { + t.Errorf("ChildSlots[1].MDLContainer = %q, want %q", def.ChildSlots[1].MDLContainer, "FILTERBAR") + } + + // Verify datasource mapping + dsMappings := findMapping(def.PropertyMappings, "datasource") + if dsMappings == nil { + t.Fatal("datasource mapping not found") + } + if dsMappings.Operation != "datasource" { + t.Errorf("datasource operation = %q, want %q", dsMappings.Operation, "datasource") + } + + // Verify attribute mapping + attrMapping := findMapping(def.PropertyMappings, "myAttribute") + if attrMapping == nil { + t.Fatal("myAttribute mapping not found") + } + if attrMapping.Operation != "attribute" || attrMapping.Source != "Attribute" { + t.Errorf("myAttribute: operation=%q source=%q, want operation=attribute source=Attribute", + attrMapping.Operation, attrMapping.Source) + } + + // Verify boolean with default value + boolMapping := findMapping(def.PropertyMappings, "showHeader") + if boolMapping == nil { + t.Fatal("showHeader mapping not found") + } + if boolMapping.Value != "true" { + t.Errorf("showHeader value = %q, want %q", boolMapping.Value, "true") + } + + // Verify selection with default + selMapping := findMapping(def.PropertyMappings, "itemSelection") + if selMapping == nil { + t.Fatal("itemSelection mapping not found") + } + if selMapping.Operation != "selection" || selMapping.Default != "Single" { + t.Errorf("itemSelection: operation=%q default=%q, want operation=selection default=Single", + selMapping.Operation, selMapping.Default) + } +} + +func TestGenerateDefJSON_SkipsComplexTypes(t *testing.T) { + mpkDef := &mpk.WidgetDefinition{ + ID: "com.example.Complex", + Name: "Complex", + Properties: []mpk.PropertyDef{ + {Key: "myAction", Type: "action"}, + {Key: "myExpr", Type: "expression"}, + {Key: "myTemplate", Type: "textTemplate"}, + {Key: "myIcon", Type: "icon"}, + {Key: "myObj", Type: "object"}, + }, + } + + def := generateDefJSON(mpkDef, "COMPLEX") + + // Complex types should be skipped + if len(def.PropertyMappings) != 0 { + t.Errorf("PropertyMappings count = %d, want 0 (complex types should be skipped)", len(def.PropertyMappings)) + } + if len(def.ChildSlots) != 0 { + t.Errorf("ChildSlots count = %d, want 0", len(def.ChildSlots)) + } +} + +func findMapping(mappings []executor.PropertyMapping, key string) *executor.PropertyMapping { + for i := range mappings { + if mappings[i].PropertyKey == key { + return &mappings[i] + } + } + return nil +} diff --git a/cmd/mxcli/lsp.go b/cmd/mxcli/lsp.go index 111b437..f7cc58d 100644 --- a/cmd/mxcli/lsp.go +++ b/cmd/mxcli/lsp.go @@ -56,6 +56,10 @@ type mdlServer struct { mxcliPath string // Path to mxcli binary (default: os.Executable()) workspaceRoot string // Workspace folder path (filesystem path, not URI) cache *lspCache // Subprocess result cache + + // Widget completion cache (lazily populated) + widgetCompletionsOnce sync.Once + widgetCompletionItems []protocol.CompletionItem } func newMDLServer(client protocol.Client) *mdlServer { diff --git a/cmd/mxcli/lsp_completion.go b/cmd/mxcli/lsp_completion.go index 68f3698..95d1e09 100644 --- a/cmd/mxcli/lsp_completion.go +++ b/cmd/mxcli/lsp_completion.go @@ -6,6 +6,7 @@ import ( "context" "strings" + "github.com/mendixlabs/mxcli/mdl/executor" "go.lsp.dev/protocol" "go.lsp.dev/uri" ) @@ -38,7 +39,7 @@ func (s *mdlServer) Completion(ctx context.Context, params *protocol.CompletionP }, nil } - items := mdlCompletionItems(linePrefixUpper) + items := s.mdlCompletionItems(linePrefixUpper) return &protocol.CompletionList{ IsIncomplete: false, Items: items, @@ -51,7 +52,7 @@ func (s *mdlServer) CompletionResolve(ctx context.Context, params *protocol.Comp } // mdlCompletionItems returns completion items filtered by context. -func mdlCompletionItems(linePrefixUpper string) []protocol.CompletionItem { +func (s *mdlServer) mdlCompletionItems(linePrefixUpper string) []protocol.CompletionItem { var items []protocol.CompletionItem // After CREATE, suggest object types and CREATE snippets only @@ -67,14 +68,41 @@ func mdlCompletionItems(linePrefixUpper string) []protocol.CompletionItem { return items } - // General context: all generated keywords + snippets + // General context: all generated keywords + snippets + widget types items = append(items, mdlGeneratedKeywords...) items = append(items, mdlStatementSnippets...) items = append(items, mdlCreateSnippets...) + items = append(items, s.widgetRegistryCompletions()...) return items } +// widgetRegistryCompletions returns completion items for registered widget types, +// including user-defined widgets from global (~/.mxcli/widgets/) and project-level +// (.mxcli/widgets/) directories. +// NOTE: Cached via sync.Once — new .def.json files added while the LSP server is +// running will not appear until the server is restarted. +func (s *mdlServer) widgetRegistryCompletions() []protocol.CompletionItem { + s.widgetCompletionsOnce.Do(func() { + registry, err := executor.NewWidgetRegistry() + if err != nil { + return + } + if err := registry.LoadUserDefinitions(s.mprPath); err != nil { + // Non-fatal: user definitions are optional + _ = err + } + for _, def := range registry.All() { + s.widgetCompletionItems = append(s.widgetCompletionItems, protocol.CompletionItem{ + Label: def.MDLName, + Kind: protocol.CompletionItemKindClass, + Detail: "Pluggable widget: " + def.WidgetID, + }) + } + }) + return s.widgetCompletionItems +} + // mdlCreateContextKeywords are object types suggested after CREATE. // These are hand-written because they require semantic knowledge of what can be created. var mdlCreateContextKeywords = []protocol.CompletionItem{ diff --git a/docs-site/src/internals/widget-templates.md b/docs-site/src/internals/widget-templates.md index 99b4548..78ea12f 100644 --- a/docs-site/src/internals/widget-templates.md +++ b/docs-site/src/internals/widget-templates.md @@ -1,103 +1,305 @@ # Widget Template System -Pluggable widgets (DataGrid2, ComboBox, Gallery, etc.) require embedded template definitions for correct BSON serialization. This page explains how the template system works. +Pluggable widgets (DataGrid2, ComboBox, Gallery, etc.) require embedded template definitions for correct BSON serialization. This page explains how the template system works, from extraction through runtime loading and property mapping. ## Why Templates Are Needed -Pluggable widgets in Mendix are defined by two components: +Pluggable widgets in Mendix are defined by two BSON components stored inside each widget instance: -1. **Type** (`PropertyTypes` schema) -- defines what properties the widget accepts -2. **Object** (`WidgetObject` defaults) -- provides default values for all properties +1. **Type** (`CustomWidgets$CustomWidgetType`) -- defines the widget's PropertyTypes schema (what properties exist, their value types) +2. **Object** (`CustomWidgets$WidgetObject`) -- provides a valid instance with default values for all properties -Both must be present in the BSON output. The type defines the schema; the object provides a valid instance. If the object's property structure does not match the type's schema, Studio Pro reports **CE0463 "widget definition changed"**. +Both must be present. The Object's `TypePointer` references the Type's `$ID`, and each `WidgetProperty.TypePointer` in the Object references the corresponding `WidgetPropertyType.$ID` in the Type. If these cross-references are broken or any property is missing, Studio Pro reports **CE0463 "widget definition changed"**. + +Building these structures programmatically is error-prone (50+ PropertyTypes, nested ValueTypes, TextTemplate structures, etc.), so mxcli clones them from known-good templates extracted from Studio Pro. ## Template Location -Templates are embedded in the binary via Go's `go:embed` directive: +### Embedded Templates (built into binary) ``` sdk/widgets/ -├── loader.go # Template loading with go:embed +├── loader.go # Template loading with go:embed +├── mpk/mpk.go # .mpk ZIP parsing for augmentation +├── definitions/ # Widget definition files (.def.json) +│ ├── combobox.def.json +│ └── gallery.def.json └── templates/ - ├── README.md # Template extraction requirements - ├── 10.6.0/ # Templates by Mendix version - │ ├── DataGrid2.json - │ ├── ComboBox.json - │ ├── Gallery.json - │ └── ... - └── 11.6.0/ - ├── DataGrid2.json - └── ... + └── mendix-11.6/ # Templates by Mendix version + ├── combobox.json + ├── datagrid.json + ├── gallery.json + └── datagrid-*-filter.json ``` -Each JSON file contains both the `type` and `object` fields for a specific widget at a specific Mendix version. +### User Templates (3-tier priority) + +| Priority | Location | Scope | +|----------|----------|-------| +| 1 (highest) | `/.mxcli/widgets/*.json` | Project-specific | +| 2 | `~/.mxcli/widgets/*.json` | Global (all projects) | +| 3 (lowest) | `sdk/widgets/templates/` (embedded) | Built-in | -## Template Structure +## Template JSON Structure -A template JSON file looks like: +Each template file contains both the Type and Object structures converted from BSON to JSON: ```json { + "widgetId": "com.mendix.widget.web.combobox.Combobox", + "name": "Combo box", + "version": "11.6.0", + "extractedFrom": "PageTemplates.Customer_NewEdit", "type": { - "$Type": "CustomWidgets$WidgetPropertyTypes", - "Properties": [ - { "Name": "columns", "Type": "Object", ... }, - { "Name": "showLabel", "Type": "Boolean", ... } + "$ID": "aa000000000000000000000000000001", + "$Type": "CustomWidgets$CustomWidgetType", + "WidgetId": "com.mendix.widget.web.combobox.Combobox", + "PropertyTypes": [ + { + "$ID": "aa000000000000000000000000000010", + "$Type": "CustomWidgets$WidgetPropertyType", + "PropertyKey": "attributeEnumeration", + "ValueType": { + "$ID": "aa000000000000000000000000000011", + "Type": "Attribute", + "DefaultValue": "" + } + } ] }, "object": { + "$ID": "aa000000000000000000000000000100", "$Type": "CustomWidgets$WidgetObject", + "TypePointer": "aa000000000000000000000000000001", "Properties": [ - { "Name": "columns", "Value": [] }, - { "Name": "showLabel", "Value": true } + 2, + { + "$ID": "aa000000000000000000000000000110", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "aa000000000000000000000000000010", + "Value": { + "$ID": "aa000000000000000000000000000111", + "$Type": "CustomWidgets$WidgetValue", + "Action": { "$Type": "Forms$NoAction", "DisabledDuringExecution": true }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "PrimitiveValue": "", + "Selection": "None", + "TextTemplate": null, + "Widgets": [2], + "XPathConstraint": "" + } + } ] } } ``` -## Extracting Templates +### Key Cross-References -Templates must be extracted from **Studio Pro-created widgets**, not generated programmatically. The extraction process: +``` +Type.PropertyTypes[].$ID <-- Object.Properties[].TypePointer + (WidgetPropertyType) (WidgetProperty points to its type) -1. Create a widget instance in Studio Pro -2. Save the project -3. Extract the BSON from the `.mpr` file -4. Convert the `type` and `object` sections to JSON -5. Save as a template file +Type.$ID <-- Object.TypePointer + (CustomWidgetType) (WidgetObject points to its type) -This ensures the property structure exactly matches what Studio Pro expects. +Type.PropertyTypes[].ValueType.$ID <-- Object.Properties[].Value.TypePointer + (ValueType definition) (WidgetValue points to its value type) +``` -> **Important:** Programmatically generated templates often have subtle differences in property ordering, default values, or nested structures that cause CE0463 errors. Always extract from Studio Pro. +These cross-references are maintained through ID remapping at load time (see below). ## Loading Templates at Runtime -The `loader.go` file provides functions to load templates: +### Entry Point ```go -// Load a widget template for the project's Mendix version -template, err := widgets.LoadTemplate("DataGrid2", mendixVersion) +bsonType, bsonObject, propertyTypeIDs, objectTypeID, err := + widgets.GetTemplateFullBSON(widgetID, mpr.GenerateID, projectPath) +``` + +### 3-Phase Pipeline (`loader.go`) + +#### Phase 1: Collect IDs and Generate Mapping + +`collectIDs()` recursively walks both `type` and `object` JSON, finds every `$ID` field, and creates a mapping from old template IDs to freshly generated UUIDs: + +``` +Template $ID (static) -> New UUID (runtime) +"aa000000000000000000000000000001" -> "a1b2c3d4e5f6..." (mpr.GenerateID()) +"aa000000000000000000000000000010" -> "f7e8d9c0b1a2..." +... +``` + +This ensures each widget instance gets unique IDs while preserving internal cross-references. + +#### Phase 2: Convert Type JSON to BSON + +`jsonToBSONWithMappingAndObjectType()` converts the Type JSON to `bson.D`, performing three tasks simultaneously: + +1. **ID replacement**: Every `$ID` field is looked up in the mapping, converted to binary GUID format via `hexToIDBlob()` (with Microsoft GUID byte-swap for the first 3 segments) +2. **String ID references**: Any 32-char hex string value that appears in the mapping is also converted to binary (these are cross-references between elements) +3. **PropertyTypeIDMap extraction**: For each `CustomWidgets$WidgetPropertyType` node, records: + +```go +PropertyTypeIDMap["attributeEnumeration"] = PropertyTypeIDEntry{ + PropertyTypeID: "f7e8d9c0b1a2...", // new ID of the PropertyType + ValueTypeID: "c3d4e5f6a7b8...", // new ID of the ValueType + DefaultValue: "", // from ValueType.DefaultValue + ValueType: "Attribute", // from ValueType.Type + ObjectTypeID: "...", // for nested object list properties + NestedPropertyIDs: {...}, // property IDs within nested ObjectType +} +``` + +This map is the bridge between `.def.json` property keys and the BSON structure. + +#### Phase 3: Convert Object JSON to BSON + +`jsonToBSONObjectWithMapping()` converts the Object JSON using the **same** ID mapping. Special handling for `TypePointer` fields ensures they point to the correct new IDs in the Type. + +#### Placeholder Leak Detection + +After conversion, `containsPlaceholderID()` checks for any remaining `aa000000`-prefix IDs (binary or string). If found, the load fails immediately rather than producing a corrupt MPR. + +### MPK Augmentation + +Before the 3-phase pipeline, `augmentFromMPK()` checks if the project has a newer version of the widget: + +``` +1. FindMPK(projectDir, widgetID) + -> Scan project/widgets/*.mpk, parse package.xml to match widget ID +2. ParseMPK(mpkPath) + -> Extract XML property definitions from the .mpk ZIP +3. AugmentTemplate(clone, mpkDef) + -> Deep-clone the cached template (never mutate cache) + -> Add properties present in .mpk but missing from template + -> Remove properties in template but absent from .mpk ``` -The loader searches for the most specific version match, falling back to earlier versions if an exact match is not available. +This reduces CE0463 errors from widget version drift. The `.mpk` in the project's `widgets/` folder is the source of truth for which properties should exist. -## JSON Templates vs BSON Serialization +### JSON to BSON Conversion Rules -When editing template JSON files, use standard JSON conventions: +| JSON Type | BSON Type | +|-----------|-----------| +| `"string"` | string | +| `float64` (whole number) | `int32` | +| `float64` (decimal) | `float64` | +| `true`/`false` | boolean | +| `null` | null | +| `[]` | empty `bson.A` | +| `[2, ...]` | `bson.A{int32(2), ...}` (array with version marker) | +| 32-char hex string in ID mapping | `[]byte` (binary GUID) | -- Empty arrays are `[]` (not `[3]`) -- Booleans are `true`/`false` -- Strings are `"quoted"` +**Important**: Empty arrays in template JSON are `[]`, not `[3]`. The BSON array version markers (`int32(2)` for non-empty, `int32(3)` for empty) are added during widget serialization, not during template loading. -The array count prefixes (`[3, ...]`) required by Mendix BSON are added automatically during serialization. Writing `[2]` in a JSON template creates an array containing the integer 2, not an empty BSON array. +## How Operations Modify Template BSON + +After loading, the pluggable widget engine applies property mappings. Each operation locates the target WidgetProperty by matching `TypePointer`: + +``` +updateWidgetPropertyValue(obj, propTypeIDs, "datasource", updateFn) + | + +-- Look up propTypeIDs["datasource"].PropertyTypeID -> "f7e8d9c0b1a2..." + | + +-- Scan obj.Properties[] array + | For each WidgetProperty: + | matchesTypePointer(prop, "f7e8d9c0b1a2...") + | -> prop.TypePointer (binary GUID) -> BlobToUUID() -> compare + | -> Match found! + | + +-- Extract prop.Value (bson.D) + +-- Apply updateFn to modify specific fields: + opAttribute -> sets Value.AttributeRef + opAssociation -> sets Value.AttributeRef + Value.EntityRef + opPrimitive -> sets Value.PrimitiveValue + opDatasource -> replaces Value.DataSource + opSelection -> sets Value.Selection + opWidgets -> replaces Value.Widgets array +``` + +All modifications produce new `bson.D` values (immutable style). + +## Extracting New Templates + +### Important: Use Studio Pro-Created Widgets + +Always extract templates from widgets that have been **created or updated by Studio Pro**. Programmatically generated templates often have subtle differences in property ordering, default values, or nested structures (especially `TextTemplate` properties that require `Forms$ClientTemplate` structure instead of `null`). + +### Extraction Process + +1. **Create the widget in Studio Pro** -- add the widget to a page, configure with default settings +2. **If updating**: right-click and select "Update widget" if Studio Pro shows "widget definition has changed" +3. **Extract using mxcli**: + +```bash +# Extract from MPR (manual method) +mxcli bson dump -p App.mpr --type page --object "Module.TestPage" --format json + +# Extract skeleton .def.json from .mpk (automated) +mxcli widget extract --mpk widgets/MyWidget.mpk +# Output: .mxcli/widgets/mywidget.def.json +``` + +4. From the JSON dump, extract the `Type` and `Object` fields of the `CustomWidgets$CustomWidget` and save as the template JSON file + +### Verifying Templates + +```bash +# Create a test page with the widget +mxcli -p test.mpr -c "CREATE PAGE Test.TestPage ... MYWIDGET ..." + +# Check for errors (should have no CE0463) +~/.mxcli/mxbuild/*/modeler/mx check test.mpr + +# Compare BSON structure if issues persist +mxcli bson dump -p test.mpr --type page --object "Test.TestPage" --format ndsl +``` ## Debugging CE0463 Errors If a page fails with CE0463 after creation: 1. Create the same widget manually in Studio Pro -2. Extract its BSON from the saved project -3. Compare the template's `object` properties against the Studio Pro version -4. Look for missing properties, wrong default values, or incorrect nesting -5. Update the template JSON to match +2. Extract its BSON with `mxcli bson dump --format ndsl` +3. Compare against the mxcli-generated widget's BSON +4. Look for: + - Missing properties (PropertyType exists in Type but no corresponding WidgetProperty in Object) + - Wrong default values (especially `TextTemplate` properties that should not be `null`) + - Stale properties from an older widget version +5. Update the template JSON to match, or let MPK augmentation handle version drift + +See `.claude/skills/debug-bson.md` for the detailed BSON debugging workflow. + +## TextTemplate Property Requirements + +Properties with `"Type": "TextTemplate"` require a proper `Forms$ClientTemplate` structure -- they **cannot** be `null`: + +```json +"TextTemplate": { + "$ID": "", + "$Type": "Forms$ClientTemplate", + "Fallback": { "$ID": "", "$Type": "Texts$Text", "Items": [] }, + "Parameters": [], + "Template": { "$ID": "", "$Type": "Texts$Text", "Items": [] } +} +``` + +Empty arrays here must be `[]`, not `[2]`. + +## Key Source Files -See the debug workflow in `.claude/skills/debug-bson.md` for detailed steps. +| File | Purpose | +|------|---------| +| `sdk/widgets/loader.go` | Template loading, 3-phase ID remapping, MPK augmentation | +| `sdk/widgets/mpk/mpk.go` | .mpk ZIP parsing, XML property extraction, FindMPK | +| `sdk/widgets/definitions/*.def.json` | Built-in widget definitions | +| `sdk/widgets/templates/mendix-11.6/*.json` | Embedded BSON templates | +| `mdl/executor/widget_engine.go` | PluggableWidgetEngine, 6 operations, Build() pipeline | +| `mdl/executor/widget_registry.go` | 3-tier registry, definition validation | +| `mdl/executor/cmd_pages_builder_input.go` | `updateWidgetPropertyValue()`, TypePointer matching | +| `cmd/mxcli/cmd_widget.go` | `mxcli widget extract/list` CLI commands | diff --git a/docs/plans/2026-03-25-pluggable-widget-engine-design.md b/docs/plans/2026-03-25-pluggable-widget-engine-design.md new file mode 100644 index 0000000..4b4a712 --- /dev/null +++ b/docs/plans/2026-03-25-pluggable-widget-engine-design.md @@ -0,0 +1,330 @@ +# Pluggable Widget Engine: 声明式 Widget 构建系统 + +**Date**: 2026-03-25 +**Status**: Implemented + +## Problem + +当前每个 pluggable widget 都需要硬编码一个 Go builder 函数(`buildComboBoxV3`, `buildGalleryV3` 等),加上 switch case 注册。新增一个 widget 需要改 4 个地方: + +1. `sdk/pages/pages_widgets_advanced.go` — 添加 WidgetID 常量 +2. `sdk/widgets/templates/` — 添加 JSON 模板 +3. `mdl/executor/cmd_pages_builder_v3_pluggable.go` — 写专属 build 函数(50-200 行) +4. `mdl/executor/cmd_pages_builder_v3.go` — 在 `buildWidgetV3()` switch 中添加 case + +这导致: +- 用户无法自行添加 pluggable widget 支持 +- 大量重复代码(30+ builder 函数共享 ~80% 骨架) +- 维护成本随 widget 数量线性增长 + +## Solution: 声明式 Widget 定义 + 通用构建引擎 + +### Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ WidgetRegistry │ +│ ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │ +│ │ combobox │ │ gallery │ │ user-custom │ │ +│ │ .def.json │ │ .def.json │ │ .def.json │ │ +│ └─────┬──────┘ └─────┬──────┘ └────────┬─────────┘ │ +│ └──────────────┼─────────────────┘ │ +│ ▼ │ +│ PluggableWidgetEngine │ +│ ┌──────────────────────────────┐ │ +│ │ 1. loadTemplate() │ │ +│ │ 2. selectMode(conditions) │ │ +│ │ 3. applyPropertyMappings() │ │ +│ │ 4. applyChildSlots() │ │ +│ │ 5. buildCustomWidget() │ │ +│ └──────────────────────────────┘ │ +│ │ │ +│ OperationRegistry │ +│ ┌─────────┬───────────┬───────────┬──────┐ │ +│ │attribute│primitive │datasource │ ... │ │ +│ │ │ │ │extend│ │ +│ └─────────┴───────────┴───────────┴──────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +### Widget Definition Format (`.def.json`) + +```json +{ + "widgetId": "com.mendix.widget.web.combobox.Combobox", + "mdlName": "COMBOBOX", + "templateFile": "combobox.json", + "defaultEditable": "Always", + + "modes": [ + { + "name": "association", + "condition": "hasDataSource", + "description": "Association mode with DataSource", + "propertyMappings": [ + { + "propertyKey": "optionsSourceType", + "value": "association", + "operation": "primitive" + }, + { + "propertyKey": "optionsSourceAssociationDataSource", + "source": "DataSource", + "operation": "datasource" + }, + { + "propertyKey": "attributeAssociation", + "source": "Attribute", + "operation": "association" + }, + { + "propertyKey": "optionsSourceAssociationCaptionAttribute", + "source": "CaptionAttribute", + "operation": "attribute" + } + ] + }, + { + "name": "default", + "description": "Enumeration mode", + "propertyMappings": [ + { + "propertyKey": "attributeEnumeration", + "source": "Attribute", + "operation": "attribute" + } + ] + } + ] +} +``` + +Gallery (with child slots): + +```json +{ + "widgetId": "com.mendix.widget.web.gallery.Gallery", + "mdlName": "GALLERY", + "templateFile": "gallery.json", + "defaultEditable": "Always", + + "propertyMappings": [ + {"propertyKey": "advanced", "value": "false", "operation": "primitive"}, + {"propertyKey": "datasource", "source": "DataSource", "operation": "datasource"}, + {"propertyKey": "itemSelection", "source": "Selection", "operation": "selection"}, + {"propertyKey": "itemSelectionMode", "value": "clear", "operation": "primitive"}, + {"propertyKey": "desktopItems", "value": "1", "operation": "primitive"}, + {"propertyKey": "tabletItems", "value": "1", "operation": "primitive"}, + {"propertyKey": "phoneItems", "value": "1", "operation": "primitive"}, + {"propertyKey": "pageSize", "value": "20", "operation": "primitive"}, + {"propertyKey": "pagination", "value": "buttons", "operation": "primitive"}, + {"propertyKey": "pagingPosition", "value": "below", "operation": "primitive"}, + {"propertyKey": "showEmptyPlaceholder", "value": "none", "operation": "primitive"}, + {"propertyKey": "onClickTrigger", "value": "single", "operation": "primitive"} + ], + + "childSlots": [ + {"propertyKey": "content", "mdlContainer": "TEMPLATE", "operation": "widgets"}, + {"propertyKey": "emptyPlaceholder", "mdlContainer": "EMPTYPLACEHOLDER", "operation": "widgets"}, + {"propertyKey": "filtersPlaceholder", "mdlContainer": "FILTERSPLACEHOLDER", "operation": "widgets"} + ] +} +``` + +### 6 Operation Types + +All existing pluggable widget builders use combinations of these 6 operations: + +| Operation | Function | Input | Description | +|-----------|----------|-------|-------------| +| `attribute` | `setAttributeRef()` | `source` → MDL Attribute prop | Sets `AttributeRef` with qualified path (`Module.Entity.Attr`) | +| `association` | `setAssociationRef()` | `source` → MDL Attribute prop | Sets association path + entity ref | +| `primitive` | `setPrimitiveValue()` | `value` or `source` | Sets `PrimitiveValue` string (enum selection, boolean, etc.) | +| `datasource` | `setDataSource()` | `source` → MDL DataSource prop | Builds and sets `DataSource` object | +| `selection` | `setSelectionMode()` | `value` or `source` | Set widget selection mode (Single/Multi) | +| `widgets` | inline child BSON | `childSlots` config | Embeds serialized child widgets into `Widgets` array | + +Operations are registered in an `OperationRegistry` and new types can be added without modifying the engine. + +### Definition Schema (`WidgetDefinition`) + +```go +type WidgetDefinition struct { + WidgetID string `json:"widgetId"` + MDLName string `json:"mdlName"` + TemplateFile string `json:"templateFile"` + DefaultEditable string `json:"defaultEditable"` + DefaultSelection string `json:"defaultSelection,omitempty"` + + // Simple case: single mode + PropertyMappings []PropertyMapping `json:"propertyMappings,omitempty"` + ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"` + + // Multi-mode case (e.g., ComboBox enum vs association) + // Uses slice instead of map to preserve evaluation order (first-match-wins semantics) + Modes []WidgetMode `json:"modes,omitempty"` +} + +type WidgetMode struct { + Condition string `json:"condition,omitempty"` + Description string `json:"description,omitempty"` + PropertyMappings []PropertyMapping `json:"propertyMappings"` + ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"` +} + +type PropertyMapping struct { + PropertyKey string `json:"propertyKey"` // Template property key + Source string `json:"source,omitempty"` // MDL AST property name + Value string `json:"value,omitempty"` // Static value (mutually exclusive with Source) + Operation string `json:"operation"` // attribute|association|primitive|datasource + Default string `json:"default,omitempty"` // Default value if source is empty +} + +type ChildSlotMapping struct { + PropertyKey string `json:"propertyKey"` // Template property key for widget list + MDLContainer string `json:"mdlContainer"` // MDL child container name (TEMPLATE, FILTER) + Operation string `json:"operation"` // Always "widgets" +} +``` + +### Critical: Property Mapping Order Dependency + +**The engine processes `propertyMappings` in array order.** Some operations depend on side effects of earlier ones: + +- `datasource` sets `pageBuilder.entityContext` as a side effect +- `association` reads `pageBuilder.entityContext` to resolve the target entity + +Therefore, in any mode that uses both, **`datasource` must come before `association`** in the mappings array. Getting this wrong produces silently incorrect BSON (wrong entity reference). + +### Operation Validation + +Operation names in `.def.json` files are validated at load time against the 6 known operations: `attribute`, `association`, `primitive`, `selection`, `datasource`, `widgets`. Invalid operation names produce an error when `NewWidgetRegistry()` or `LoadUserDefinitions()` runs, rather than failing silently at build time. + +### Mode Selection Conditions + +Built-in conditions (extensible): + +| Condition | Logic | +|-----------|-------| +| `hasDataSource` | `w.GetDataSource() != nil` | +| `hasAttribute` | `w.GetAttribute() != ""` | +| `hasProp:X` | `w.GetStringProp("X") != ""` | +| (none) | Fallback — first no-condition mode wins if multiple exist | + +### Engine Flow + +```go +func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (*pages.CustomWidget, error) { + // 1. Load template + tmplType, tmplObj, tmplIDs, objTypeID, err := widgets.GetTemplateFullBSON( + def.WidgetID, mpr.GenerateID, e.projectPath) + + // 2. Select mode + mode := e.selectMode(def, w) + + // 3. Apply property mappings + propTypeIDs := convertPropertyTypeIDs(tmplIDs) + updatedObj := tmplObj + for _, mapping := range mode.PropertyMappings { + op := e.operations.Get(mapping.Operation) + value := e.resolveSource(mapping, w) + updatedObj = op.Apply(updatedObj, propTypeIDs, mapping.PropertyKey, value, e.buildCtx) + } + + // 4. Apply child slots + for _, slot := range mode.ChildSlots { + childBSONs := e.buildChildWidgets(w, slot.MDLContainer) + updatedObj = e.applyWidgetSlot(updatedObj, propTypeIDs, slot.PropertyKey, childBSONs) + } + + // 5. Build CustomWidget + return &pages.CustomWidget{ + BaseWidget: pages.BaseWidget{...}, + Editable: def.DefaultEditable, + RawType: tmplType, + RawObject: updatedObj, + PropertyTypeIDMap: propTypeIDs, + ObjectTypeID: objTypeID, + }, nil +} +``` + +### Integration with buildWidgetV3() + +```go +func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { + switch strings.ToUpper(w.Type) { + // Native Mendix widgets — keep hardcoded (different BSON structure) + case "DATAVIEW": + return pb.buildDataViewV3(w) + case "LISTVIEW": + return pb.buildListViewV3(w) + case "TEXTBOX": + return pb.buildTextBoxV3(w) + // ... other native widgets + + default: + // All pluggable widgets go through declarative engine + if def, ok := pb.widgetRegistry.Get(strings.ToUpper(w.Type)); ok { + return pb.pluggableEngine.Build(def, w) + } + return nil, fmt.Errorf("unsupported widget type: %s", w.Type) + } +} +``` + +### User Extension Points + +**File locations (project-level takes priority):** + +``` +project/ +└── .mxcli/ + └── widgets/ + ├── my-rating-widget.def.json # Widget definition + └── my-rating-widget.json # BSON template + +~/.mxcli/ +└── widgets/ + ├── shared-chart.def.json # Global widget definition + └── shared-chart.json # Global BSON template +``` + +**Template extraction tool:** + +```bash +# Extract template from .mpk widget package +mxcli widget extract --mpk path/to/widget.mpk + +# Generates: +# .mxcli/widgets/.json (template with type + object) +# .mxcli/widgets/.def.json (skeleton definition) +``` + +### Migration Plan + +| Phase | Scope | Deliverable | +|-------|-------|-------------| +| 1 | Engine skeleton + ComboBox migration | Validate the approach with simplest widget | +| 2 | Migrate Gallery, DataGrid, 4 Filters | Delete 6 hardcoded builder functions (~600 lines) | +| 3 | User extension: registry scan + extract tool | Users can add custom widget support | +| 4 | LSP integration | Completion, hover, diagnostics for custom widgets | + +### What Stays Hardcoded + +**Native Mendix widgets** (TextBox, DataView, ListView, LayoutGrid, Container, etc.) use a fundamentally different BSON structure (`Forms$TextBox`, `Forms$DataView`) — NOT `CustomWidgets$CustomWidget`. These stay as hardcoded builders because: +- They don't use the template system +- Their BSON structure varies significantly per widget type +- There are ~20 of them and they're stable (rarely new ones added) + +### Risk Analysis + +| Risk | Mitigation | +|------|------------| +| Complex widgets may not fit declarative model | `modes` + extensible operations provide escape hatches | +| Template version drift | Existing `augment.go` handles .mpk sync, works unchanged | +| Performance regression | Template loading is already cached; engine adds minimal overhead | +| User-provided templates may be invalid | Validate on load: check type+object sections exist, PropertyKey coverage | +| MPK zip-bomb attack | `ParseMPK` enforces per-file (50MB) and total (200MB) extraction limits | +| Invalid operation names in .def.json | Validated at load time, not build time — immediate feedback | +| Engine init failure retried on every widget | Init error cached; subsequent widgets skip immediately | diff --git a/mdl-examples/doctype-tests/17-custom-widget-examples.mdl b/mdl-examples/doctype-tests/17-custom-widget-examples.mdl new file mode 100644 index 0000000..8eabe76 --- /dev/null +++ b/mdl-examples/doctype-tests/17-custom-widget-examples.mdl @@ -0,0 +1,207 @@ +-- ============================================================================ +-- Custom Widget Examples - GALLERY & COMBOBOX +-- ============================================================================ +-- +-- Tests for pluggable widget MDL syntax (GALLERY, COMBOBOX). +-- +-- Test matrix: +-- 1. GALLERY basic — DATABASE datasource + TEMPLATE (PASS) +-- 2. GALLERY with FILTER — TEXTFILTER in FILTER block (KNOWN BUG: CE0463) +-- 3. COMBOBOX enum — Enum attribute (KNOWN BUG: CE1613) +-- 4. COMBOBOX association — DataSource + CaptionAttribute (FIXED: Issue #21) +-- +-- Known engine bugs (do not affect MDL syntax correctness): +-- - CE0463 on TEXTFILTER: embedded template property count mismatch +-- - CE1613 on COMBOBOX enum: enum attribute written as association pointer (pending) +-- +-- ============================================================================ + +-- MARK: Setup + +-- ============================================================================ +-- Entities & Enumerations +-- ============================================================================ + +CREATE ENUMERATION MyFirstModule.Priority ( + LOW 'Low', + MEDIUM 'Medium', + HIGH 'High', + CRITICAL 'Critical' +); +/ + +@Position(100,300) +CREATE PERSISTENT ENTITY MyFirstModule.Category ( + Name: String(200) NOT NULL, + Description: String(500) +); + +@Position(300,300) +CREATE PERSISTENT ENTITY MyFirstModule.Task ( + Title: String(200) NOT NULL, + Description: String(1000), + Priority: Enumeration(MyFirstModule.Priority), + DueDate: DateTime, + IsCompleted: Boolean DEFAULT false +); + +CREATE ASSOCIATION MyFirstModule.Task_Category +FROM MyFirstModule.Task TO MyFirstModule.Category +TYPE Reference +OWNER Both; + +-- MARK: GALLERY basic (PASS) + +-- ============================================================================ +-- Test 1: GALLERY basic — DATABASE datasource + TEMPLATE with DYNAMICTEXT +-- Result: PASS (0 errors) +-- ============================================================================ + +/** + * Basic Gallery showing Task cards with title and description. + * Validates: GALLERY widget, DATABASE datasource, TEMPLATE with DYNAMICTEXT. + */ +CREATE PAGE MyFirstModule.P_Gallery_Basic +( + Title: 'Gallery Basic', + Layout: Atlas_Core.Atlas_Default, + Folder: 'CustomWidgets' +) +{ + LAYOUTGRID lgMain { + ROW row1 { + COLUMN col1 (DesktopWidth: 12) { + GALLERY galTasks ( + DataSource: DATABASE MyFirstModule.Task SORT BY Title ASC + ) { + TEMPLATE tpl1 { + DYNAMICTEXT dtTitle (Content: '{1}', ContentParams: [{1} = Title], RenderMode: H4) + DYNAMICTEXT dtDesc (Content: '{1}', ContentParams: [{1} = Description]) + } + } + } + } + } +} + +-- MARK: GALLERY with FILTER (KNOWN BUG: CE0463) + +-- ============================================================================ +-- Test 2: GALLERY with FILTER — TEXTFILTER in FILTER block +-- Result: CE0463 on TextFilter (template property count mismatch) +-- ============================================================================ + +/** + * Gallery with filter bar for text search. + * KNOWN BUG: CE0463 on TEXTFILTER due to embedded template mismatch. + * The MDL syntax is correct; the engine template needs re-extraction. + */ +CREATE PAGE MyFirstModule.P_Gallery_Filtered +( + Title: 'Gallery with Filter', + Layout: Atlas_Core.Atlas_Default, + Folder: 'CustomWidgets' +) +{ + LAYOUTGRID lgMain { + ROW row1 { + COLUMN col1 (DesktopWidth: 12) { + GALLERY galFiltered ( + DataSource: DATABASE MyFirstModule.Task SORT BY DueDate DESC + ) { + TEMPLATE tpl1 { + DYNAMICTEXT dtTitle (Content: '{1}', ContentParams: [{1} = Title], RenderMode: H4) + DYNAMICTEXT dtPriority (Content: 'Priority: {1}', ContentParams: [{1} = Priority]) + } + FILTER flt1 { + TEXTFILTER tfSearch (Attribute: Title) + } + } + } + } + } +} + +-- MARK: COMBOBOX enum (KNOWN BUG: CE1613) + +-- ============================================================================ +-- Test 3: COMBOBOX enum mode — Attribute bound to an enumeration +-- Result: CE1613 (association pointer written instead of attribute reference) +-- ============================================================================ + +/** + * Task edit form with a COMBOBOX selecting the Priority enumeration. + * KNOWN BUG: CE1613 — engine writes enum attribute as association. + * + * @param $Task The task to edit + */ +CREATE PAGE MyFirstModule.P_ComboBox_Enum +( + Params: { + $Task: MyFirstModule.Task + }, + Title: 'ComboBox Enum', + Layout: Atlas_Core.PopupLayout, + Folder: 'CustomWidgets' +) +{ + DATAVIEW dvTask (DataSource: $Task) { + LAYOUTGRID lg1 { + ROW row1 { + COLUMN col1 (DesktopWidth: 12) { + TEXTBOX txtTitle (Label: 'Title', Attribute: Title) + COMBOBOX cmbPriority (Label: 'Priority', Attribute: Priority) + DATEPICKER dpDue (Label: 'Due Date', Attribute: DueDate) + } + } + } + FOOTER footer1 { + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES CLOSE_PAGE, ButtonStyle: Success) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES CLOSE_PAGE, ButtonStyle: Default) + } + } +} + +-- MARK: COMBOBOX association (KNOWN BUG: CE1613) + +-- ============================================================================ +-- Test 4: COMBOBOX association mode — DataSource + CaptionAttribute +-- Result: FIXED (Issue #21) — Attribute now correctly reads back as association name +-- ============================================================================ + +/** + * Task edit form with a COMBOBOX for selecting Category via association. + * FIXED (Issue #21): Attribute now correctly roundtrips as association name. + * + * @param $Task The task to edit + */ +CREATE PAGE MyFirstModule.P_ComboBox_Assoc +( + Params: { + $Task: MyFirstModule.Task + }, + Title: 'ComboBox Association', + Layout: Atlas_Core.PopupLayout, + Folder: 'CustomWidgets' +) +{ + DATAVIEW dvTask (DataSource: $Task) { + LAYOUTGRID lg1 { + ROW row1 { + COLUMN col1 (DesktopWidth: 12) { + TEXTBOX txtTitle (Label: 'Title', Attribute: Title) + COMBOBOX cmbCategory ( + Label: 'Category', + Attribute: Task_Category, + DataSource: DATABASE MyFirstModule.Category, + CaptionAttribute: Name + ) + } + } + } + FOOTER footer1 { + ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES CLOSE_PAGE, ButtonStyle: Success) + ACTIONBUTTON btnCancel (Caption: 'Cancel', Action: CANCEL_CHANGES CLOSE_PAGE, ButtonStyle: Default) + } + } +} diff --git a/mdl-examples/doctype-tests/roundtrip-issues.md b/mdl-examples/doctype-tests/roundtrip-issues.md new file mode 100644 index 0000000..9046e3d --- /dev/null +++ b/mdl-examples/doctype-tests/roundtrip-issues.md @@ -0,0 +1,331 @@ +# MDL Roundtrip Test Issues + +> Generated: 2026-03-25 +> Baseline: `/mnt/data_sdd/gh/mxproj-GenAIDemo/App.mpr` +> mxcli: `/mnt/data_sdd/gh/mxcli/bin/mxcli` + +## Test Scope + +| File | Agent | Status | +|------|-------|--------| +| 01-domain-model-examples.mdl | A | PASS | +| 02-microflow-examples.mdl | B | PARTIAL | +| 03-page-examples.mdl | C | PARTIAL | +| 04-math-examples.mdl | B | PARTIAL | +| 05-database-connection-examples.mdl | D | PASS (env CE) | +| 07-java-action-examples.mdl | D | PASS (env CE) | +| 08-security-examples.mdl | D | PARTIAL | +| 09-constant-examples.mdl | A | PARTIAL | +| 10-odata-examples.mdl | A | PASS | +| 11-navigation-examples.mdl | D | PASS | +| 12-styling-examples.mdl | C | PASS | +| 13-business-events-examples.mdl | D | PASS (env CE) | +| 14-project-settings-examples.mdl | D | PASS | +| 16-xpath-examples.mdl | A | FAIL | +| 17-custom-widget-examples.mdl | C | PARTIAL | + +**Skipped (require Docker runtime):** 04-math-examples.tests.mdl, 06-rest-client-examples.test.mdl, 15-fragment-examples.test.mdl, microflow-spec.test.md, microflow-spec.test.mdl + +--- + +## Consolidated Bug Summary + +> 15 files tested · 7 PASS · 6 PARTIAL · 1 FAIL · 1 env-only-FAIL + +### HIGH — Real bugs, fix required + +| # | Bug | Affected Files | Category | +|---|-----|----------------|----------| +| H1 | **WHILE loop body missing from DESCRIBE** — condition shown, body activities omitted entirely | 02, 04 | DESCRIBE writer | [#18](https://github.com/engalar/mxcli/issues/18) | +| H2 | **Long → Integer type degradation** — microflow params/vars and constants both affected; values > 2^31 silently corrupted | 02, 04, 09 | Writer type mapping | [#19](https://github.com/engalar/mxcli/issues/19) | +| H3 | **XPath `[%CurrentDateTime%]` quoted as string literal** → CE0161 at runtime; MDL token not recognised by writer | 16 | Writer / XPath parser | [#20](https://github.com/engalar/mxcli/issues/20) | +| H4 | **CE0463 pluggable widget template mismatch** — Gallery TEXTFILTER, ComboBox, DataGrid2 all trigger; object property count doesn't match type schema | 03, 08, 16, 17 | Widget template | (known) | +| H5 | **ComboBox association Attribute lost** — written as association pointer, DESCRIBE shows CaptionAttribute instead of original field name | 17 | Widget writer | [#21](https://github.com/engalar/mxcli/issues/21) | + +### MEDIUM — Roundtrip gap, not data loss + +| # | Bug | Affected Files | Category | +|---|-----|----------------|----------| +| M1 | **Sequential early-return IFs → nested IF/ELSE in DESCRIBE** — changes control flow semantics | 02, 04 | DESCRIBE formatter | [#22](https://github.com/engalar/mxcli/issues/22) | +| M2 | **DataGrid column names lost** — semantic names replaced by sequential identifiers | 03 | DESCRIBE / writer | [#23](https://github.com/engalar/mxcli/issues/23) | +| M3 | **DATAVIEW SELECTION DataSource not emitted by DESCRIBE** | 03 | DESCRIBE writer | [#24](https://github.com/engalar/mxcli/issues/24) | +| M4 | **Constant COMMENT not in DESCRIBE output** | 09 | DESCRIBE writer | [#25](https://github.com/engalar/mxcli/issues/25) | +| M5 | **Date type conflated with DateTime** — no Date-only type distinction | 01 | Writer type mapping | [#26](https://github.com/engalar/mxcli/issues/26) | + +### LOW — Cosmetic / known asymmetry + +| # | Issue | Notes | +|---|-------|-------| +| L1 | `LOG 'text' + $var` → template syntax with triple-quotes | Semantically equivalent | +| L2 | XPath spacing: `$a/b` → `$a / b` | Cosmetic | +| L3 | `RETURN false/true/empty` → `RETURN $false/$true/$empty` | Dollar prefix on literals | +| L4 | `DEFAULT 0.00` → `DEFAULT 0` | Decimal precision drop | +| L5 | REST default `ON ERROR ROLLBACK` always shown | DESCRIBE verbosity | +| L6 | `CaptionParams` → `ContentParams` rename | API rename, MDL not updated | +| L7 | Gallery adds default empty `FILTER` section in DESCRIBE | Verbosity | + +### Environment CE (not mxcli bugs) + +- CE errors for missing marketplace modules (DatabaseConnector, BusinessEvents) — module not installed in baseline +- CE0106 / CE0557 — security roles not assigned to microflows/pages in test scripts +- CE6083 — theme class mismatch (test app theme vs styling examples) + +--- + +## Issues Found + + +## Agent B Results + +### 02-microflow-examples.mdl +- **exec**: OK (all 92+ microflows created, 2 java actions, 1 page, 2 modules, MOVEs executed successfully) +- **mx check**: PASS (0 errors) +- **roundtrip gaps**: + - **WHILE loop DESCRIBE missing body**: WHILE loops show the condition but the loop body activities are omitted from DESCRIBE output (e.g., M024_3_WhileLoop shows `WHILE $Counter < $N` then jumps to post-loop activities) + - **WHILE uses END LOOP**: DESCRIBE outputs `END LOOP;` instead of `END WHILE;` for WHILE loops + - **Sequential IFs become nested**: Independent sequential IF blocks (each with early RETURN) are rendered as nested IF/ELSE chains in DESCRIBE (e.g., M060_ValidateProduct: two independent IF checks for Code and Name become nested) + - **LOG with concat becomes template**: `LOG ... 'text' + $var` is stored as `'{1}' WITH ({1} = 'text' + $var)` — semantically equivalent but different syntax roundtrip + - **LOG template triple-quotes**: Template strings get triple-quoted in DESCRIBE: `'Processing order: {1}'` → `'''Processing order: {1}'''` + - **XPath path spacing**: `$Product/Price` renders as `$Product / Price` (spaces around slash) + - **RETURN literal prefix**: `RETURN false` → `RETURN $false`, `RETURN empty` → `RETURN $empty` (dollar-sign prefix on literals) + - **RETURN formatting**: `RETURN $var` has trailing newline before semicolon in DESCRIBE + - **Decimal literal truncation**: `42.00` renders as `42` in DESCRIBE + - **REST default error handling shown**: DESCRIBE always shows `ON ERROR ROLLBACK` even when not specified in input (default value) + - **M001_HelloWorld_2 FOLDER keyword**: Input specifies `FOLDER 'microflows/basic'` on CREATE, but later MOVE overrides to 'Basic'. DESCRIBE correctly shows FOLDER 'Basic'. No gap, but FOLDER keyword on CREATE is overridden by MOVE. +- **status**: PARTIAL (exec + mx check PASS, but DESCRIBE roundtrip has formatting and structural gaps) + +### 04-math-examples.mdl +- **exec**: OK (module MathTest created, IsPrime and Fibonacci microflows created) +- **mx check**: PASS (0 errors) +- **roundtrip gaps**: + - **WHILE loop body omitted from DESCRIBE**: Both IsPrime and Fibonacci have WHILE loops whose body activities are completely missing from DESCRIBE output. IsPrime's loop body (IF $Number mod $Divisor, SET $Divisor = $Divisor + 2) is not shown. Fibonacci's loop body (SET $Current, sliding window shifts, LOG DEBUG, counter increment) is not shown. + - **Long type downgraded to Integer**: Fibonacci input specifies `RETURNS Long AS $Result` and `DECLARE $Result Long = 0`, `$Previous2 Long`, `$Previous1 Long`, `$Current Long`. DESCRIBE shows all as `Integer`. This is a **data type loss** — Long → Integer conversion. + - **Sequential IFs become nested**: IsPrime and Fibonacci both have sequential early-return guard clauses that become nested IF/ELSE chains in DESCRIBE output + - **RETURN literal prefix**: `RETURN false` → `RETURN $false`, `RETURN true` → `RETURN $true`, `RETURN 0` → `RETURN $0`, `RETURN 1` → `RETURN $1` + - **LOG template triple-quotes**: Same as 02 file — template strings get triple-quoted +- **status**: PARTIAL (exec + mx check PASS, but WHILE body omission and Long→Integer type loss are significant DESCRIBE roundtrip gaps) + +### Summary of Cross-Cutting Issues + +| Issue | Severity | Category | Files Affected | +|-------|----------|----------|----------------| +| WHILE loop body missing from DESCRIBE | HIGH | DESCRIBE bug | 02, 04 | +| Long type stored as Integer | HIGH | Writer/type mapping | 04 | +| Sequential IFs rendered as nested IF/ELSE | MEDIUM | DESCRIBE formatting | 02, 04 | +| LOG concat becomes template syntax | LOW | Semantic equivalence | 02, 04 | +| LOG template triple-quoting | LOW | DESCRIBE formatting | 02, 04 | +| XPath path spacing ($a / b vs $a/b) | LOW | DESCRIBE formatting | 02 | +| RETURN literal dollar prefix | LOW | DESCRIBE formatting | 02, 04 | +| Decimal literal truncation | LOW | DESCRIBE formatting | 02 | +| REST default ON ERROR shown | LOW | DESCRIBE verbosity | 02 | +## Agent C Results + +### 03-page-examples.mdl +- **exec**: OK (all 70+ pages/snippets/microflows created, MOVEs executed) +- **mx check**: FAIL (76 errors) + - CE0557 x41: Page/snippet allowed roles not set (expected — no GRANT in script) + - CE0106 x7: Microflow allowed roles not set (expected — no GRANT in script) + - CE0463 x28: Pluggable widget definition changed (DataGrid2, ComboBox, TextFilter templates) +- **roundtrip gaps**: + - **DATAGRID column names lost**: Input uses semantic names (`colName`, `colCode`, `colPrice`) but DESCRIBE outputs sequential names (`col1`, `col2`, `col3`). Applies to all DataGrid2 pages. + - **GALLERY adds default FILTER section**: Input GALLERY without FILTER block (e.g., P019_Gallery_Simple) gets a default FILTER with DropdownFilter, DateFilter, TextFilter in DESCRIBE output. Template includes these filter widgets that were not in the original MDL. + - **GALLERY Selection defaults to Single**: Input GALLERY without Selection property (P019) gets `Selection: Single` in output. This is a sensible default, not a bug. + - **DATAVIEW DataSource: SELECTION missing in DESCRIBE**: P020_Master_Detail has `DataSource: SELECTION customerList` but DESCRIBE omits the DataSource entirely for `customerDetail` DataView. + - **Button CaptionParams → ContentParams in DESCRIBE**: Input uses `CaptionParams: [{1} = 'abc']` on ACTIONBUTTON, DESCRIBE outputs `ContentParams: [{1} = 'abc']`. Naming inconsistency (functionally equivalent). + - **Container Class/Style roundtrip OK**: P013b_Container_Basic with Class and Style properties roundtrips perfectly. + - **GroupBox roundtrip OK**: P037_GroupBox_Example with Caption, HeaderMode, Collapsible roundtrips perfectly. + - **Snippet roundtrip OK**: NavigationMenu snippet with SHOW_PAGE actions roundtrips perfectly. + - **Page parameter, Url, Folder roundtrip OK**: All page metadata roundtrips correctly. + - **MOVE roundtrip OK**: Folder changes (Pages/Archive) reflected correctly in DESCRIBE. + - **DataGrid column properties roundtrip OK**: P033b with Alignment, WrapText, Sortable, Resizable, Draggable, Hidable, ColumnWidth, Size, Visible, DynamicCellClass, Tooltip all roundtrip correctly. + - **DesignProperties on DataGrid/columns roundtrip**: Input DesignProperties on DataGrid and columns not reflected in DESCRIBE output (may be due to CE0463 template issues). + - **CE0463 on all pluggable widgets**: DataGrid2 (28 instances), ComboBox, TextFilter templates have property count mismatches. Known engine-level issue with template extraction. +- **status**: PARTIAL (structure correct, CE0463 on pluggable widgets, minor naming/default gaps) + +### 12-styling-examples.mdl +- **exec**: OK (module, entities, 6 pages, 1 snippet created; DESCRIBE/SHOW/UPDATE commands executed inline) +- **mx check**: FAIL (20 errors) + - CE0557 x6: Page allowed roles not set (expected — no GRANT in script) + - CE6083 x14: Design property not supported by theme ("Card style", "Disable row wrap", "Full width", "Border" on Container/ActionButton) +- **roundtrip gaps**: + - **Class + Style roundtrip OK**: All CSS classes and inline styles roundtrip perfectly (P001, P002, P004, P006). + - **DesignProperties roundtrip OK**: Toggle ON/OFF, multiple properties all roundtrip correctly in DESCRIBE output. + - **CE6083 is theme-level**: The design properties used in examples ("Card style", "Disable row wrap") are not defined in the GenAIDemo project's theme. The writer serializes them correctly, but mx check rejects them as unsupported. This is a test environment issue, not a serialization bug. + - **CREATE OR REPLACE page roundtrip OK**: P006_Roundtrip replaced with updated styling, DESCRIBE shows updated values correctly. + - **Combined Class + Style + DesignProperties OK**: P004_Combined_Styling with all three styling mechanisms on single widgets roundtrips correctly. +- **status**: PASS (all styling features work; CE6083 is theme mismatch, CE0557 is expected) + +### 17-custom-widget-examples.mdl +- **exec**: OK (enumeration, 2 entities, 1 association, 4 pages created) +- **mx check**: FAIL (3 errors) + - CE0463 x2: ComboBox widget definition changed (cmbPriority, cmbCategory) + - CE0463 x1: TextFilter widget definition changed (tfSearch) +- **roundtrip gaps**: + - **GALLERY basic roundtrip OK**: P_Gallery_Basic TEMPLATE with DYNAMICTEXT roundtrips correctly. Gallery adds default FILTER section (same as 03-page-examples). + - **GALLERY Selection defaults added**: Input had no Selection, output shows `Selection: Single`. + - **GALLERY FILTER with TEXTFILTER**: Input specifies `TEXTFILTER tfSearch (Attribute: Title)` but DESCRIBE shows default filter template (DropdownFilter, DateFilter, TextFilter) instead of the user-specified single TextFilter. The user's TextFilter `Attribute:` binding is lost. + - **COMBOBOX enum roundtrip OK (DESCRIBE)**: P_ComboBox_Enum DESCRIBE shows correct `Attribute: Priority`. CE0463 at mx check level. + - **COMBOBOX association Attribute wrong**: P_ComboBox_Assoc input has `Attribute: Task_Category` (association name) but DESCRIBE shows `Attribute: Name` (the CaptionAttribute value instead). The association binding is lost in roundtrip. + - **CE0463 on ComboBox**: Known template mismatch issue. The widget definition template doesn't match what the engine expects. Documented as known bug in the MDL file. +- **status**: PARTIAL (Gallery basic works, ComboBox enum roundtrip OK at DESCRIBE level but CE0463 at mx check; ComboBox association attribute lost) + +--- + +### Summary of Systemic Issues + +| Issue | Severity | Files Affected | Description | +|-------|----------|---------------|-------------| +| CE0463 pluggable widget templates | HIGH | 03, 17 | DataGrid2, ComboBox, TextFilter templates have property count mismatches. 28+ instances. | +| DATAGRID column names lost | MEDIUM | 03 | Semantic column names → sequential col1/col2/col3 in DESCRIBE | +| GALLERY adds default FILTER | LOW | 03, 17 | Galleries without explicit FILTER get default DropdownFilter+DateFilter+TextFilter | +| DATAVIEW SELECTION DataSource missing | MEDIUM | 03 | `DataSource: SELECTION widgetName` not emitted in DESCRIBE | +| COMBOBOX association Attribute lost | HIGH | 17 | Association name replaced by CaptionAttribute in DESCRIBE | +| CaptionParams → ContentParams rename | LOW | 03 | Button caption params use different property name in DESCRIBE | +| CE6083 theme design properties | LOW | 12 | Design properties not in project theme — test env issue | +| CE0557/CE0106 security roles | INFO | 03, 12 | Expected — no GRANT statements in test scripts | + +## Agent A Results + +### 01-domain-model-examples.mdl +- **exec**: OK (all 45+ elements created successfully, including enums, entities, associations, view entities, ALTER ENTITY, MOVE, EXTENDS) +- **mx check**: PASS (0 errors) +- **roundtrip gaps**: + - **Decimal DEFAULT precision lost**: Input `DEFAULT 0.00` → DESCRIBE outputs `DEFAULT 0` (affects Product.Price, SalesOrder.TotalAmount, etc.). Cosmetic only — Mendix treats them the same. + - **Enumeration value JavaDoc lost**: Input `/** Order received */ PENDING 'Pending'` → DESCRIBE outputs `PENDING 'Pending'` (per-value docs not preserved in DESCRIBE output). + - **Date vs DateTime type conflation**: Input `ReleaseDate: Date` → DESCRIBE outputs `ReleaseDate: DateTime`. The `Date` type is stored as DateTime internally but DESCRIBE doesn't distinguish. + - **VATRate.Check Boolean→String(10) conversion side-effect**: After `ALTER ENTITY DmTest.VATRate MODIFY ATTRIBUTE Check String(10)`, DESCRIBE shows `Check: String(10) DEFAULT 'true'` — the original boolean DEFAULT true was converted to string `'true'`. Expected behavior but notable. + - **VATRate position shifted**: Input `@Position(700, 300)` → DESCRIBE outputs `@Position(4450, 100)`. Position auto-adjusted after ALTER operations. + - **ALTER ENTITY attributes lack JavaDoc**: Attributes added via `ALTER ENTITY ... ADD ATTRIBUTE` don't have JavaDoc comments (e.g., `DiscountPercentage`, `LoyaltyPoints`). Expected — ALTER ADD doesn't support inline docs. + - **MOVE operation works correctly**: Customer moved to DmTest2, Country enum moved to DmTest3 — DESCRIBE confirms correct module references including cross-module enum refs (`DmTest3.Country`). + - **INDEX direction preserved**: Input `INDEX (OrderDate desc)` → DESCRIBE outputs `INDEX (OrderDate DESC)` — correct. + - **EXTENDS roundtrip clean**: Attachment (System.FileDocument), Truck (DmTest.Vehicle), ProductPhoto (System.Image) all roundtrip correctly including GENERALIZATION keyword. + - **CREATE OR MODIFY idempotent**: AppConfig second version correctly overwrites first (position, attributes, indexes all updated). + - **GetCustomerTotalSpent microflow**: DESCRIBE shows `$Stats / TotalSpent` (with spaces around `/`) instead of `$Stats/TotalSpent`. Cosmetic formatting difference. +- **status**: PASS + +### 09-constant-examples.mdl +- **exec**: OK (7 constants created, 2 modified via CREATE OR MODIFY, 1 dropped) +- **mx check**: PASS (0 errors) +- **roundtrip gaps**: + - **Long type stored as Integer**: Input `TYPE Long DEFAULT 10485760` → DESCRIBE outputs `TYPE Integer DEFAULT 10485760`. The Long constant type is not preserved — stored/displayed as Integer. This is a real gap. + - **COMMENT not shown in DESCRIBE**: Input `COMMENT 'API key for external service...'` → DESCRIBE output does not include the COMMENT field. Comments are stored but not roundtripped in DESCRIBE output. + - **CREATE OR MODIFY works correctly**: ServiceEndpoint updated from v1 URL to staging v2 URL; EnableDebugLogging changed from false to true. + - **DROP CONSTANT works**: LaunchDate successfully removed from constant list. + - **Folder not shown**: SHOW CONSTANTS shows empty Folder column for CoTest constants (none were specified in input). Expected. +- **status**: PARTIAL (Long→Integer type loss is a real bug; COMMENT not in DESCRIBE is a gap) + +### 10-odata-examples.mdl +- **exec**: OK (all OData lifecycle operations completed: CREATE, ALTER, CREATE OR MODIFY, GRANT, REVOKE, DROP for clients, services, external entities) +- **mx check**: PASS (0 errors) +- **roundtrip gaps**: + - **Lifecycle test, not roundtrip**: This file creates OData resources then drops them at the end, so final state has no OData objects to DESCRIBE. The create→modify→alter→drop lifecycle completes without errors. + - **Base entities remain**: OdTest.Customer and OdTest.Order persist after cleanup (not dropped by script). These are setup entities, not OData-specific. + - **No DESCRIBE verification possible**: Since all OData objects are dropped by end of script, roundtrip comparison of DESCRIBE output vs input is not applicable. The test validates write operations succeed, not read-back fidelity. +- **status**: PASS (lifecycle test — all operations succeed, 0 mx errors) + +### 16-xpath-examples.mdl +- **exec**: OK (4 entities, 3 associations, 22 microflows, 2 pages created) +- **mx check**: FAIL (3 errors) + - **CE0161**: "Error(s) in XPath constraint" at Retrieve object(s) activity 'Retrieve list of Order from database' — likely from `Retrieve_DateTimeToken` where `[%CurrentDateTime%]` token is quoted as a string literal in the BSON XPath. DESCRIBE shows `WHERE OrderDate >= '[%CurrentDateTime%]'` — the token is wrapped in quotes, which may cause the XPath engine to treat it as a string literal instead of a token. + - **CE0463** (x2): DataGrid2 widget definition errors on `dgOrders` and `dgCustomers` pages — known pluggable widget template issue, not XPath-related. +- **roundtrip gaps**: + - **XPath brackets stripped**: Input `WHERE [State = 'Completed']` → DESCRIBE outputs `WHERE State = 'Completed'` (brackets removed). Cosmetic difference — both forms are valid MDL syntax. + - **RETURN true → RETURN $true**: Input `RETURN true;` → DESCRIBE outputs `RETURN $true;`. The boolean literal is represented as variable `$true`. + - **XPath functions roundtrip correctly**: `contains()`, `starts-with()`, `not()`, `true()`, `false()` all preserved in DESCRIBE output. + - **Association paths preserved**: `XpathTest.Order_Customer/XpathTest.Customer/Name` roundtrips correctly. + - **Variable paths preserved**: `$Customer/Name` in XPath roundtrips correctly. + - **Token quoting**: `[%CurrentDateTime%]` appears as `'[%CurrentDateTime%]'` in DESCRIBE — wrapped in string quotes. This may be the cause of CE0161. + - **System.owner token**: `System.owner = '[%CurrentUser%]'` roundtrips correctly in DESCRIBE. + - **Parenthesized logic preserved**: Complex grouping `($IgnoreAfterDate or OrderDate >= $AfterDate)` roundtrips correctly. + - **Pages CE0463**: DataGrid2 widgets `dgOrders` and `dgCustomers` fail with CE0463 — this is a known pluggable widget template issue, not specific to XPath functionality. +- **status**: FAIL (CE0161 XPath constraint error on DateTime token; CE0463 on pages) +## Agent D Results + +### 05-database-connection-examples.mdl +- **exec**: OK +- **mx check**: FAIL (expected): 4 errors — "We couldn't find the External Database Connector module in your app" at 4 Execute database query action activities +- **roundtrip gaps**: + - MinimalDB: DESCRIBE omits empty `BEGIN/END` block (input has `BEGIN END;`), equivalent semantically + - F1Database: DESCRIBE matches input exactly (all 8 queries, parameters, mappings) + - Microflows (GetAllDrivers, GetDriversDynamic, GetDriversByNationality): DESCRIBE matches input + - MOVE CONSTANT/DATABASE CONNECTION to folders: executed without error (no DESCRIBE verification for folder placement) +- **notes**: CE errors are expected — the test project lacks the External Database Connector marketplace module. Not an mxcli bug. +- **status**: PASS (roundtrip correct, CE errors are environment-dependent) + +### 07-java-action-examples.mdl +- **exec**: OK — created 25 java actions, 40+ microflows, 1 page +- **mx check**: FAIL (expected): 25 CE0106 errors — "At least one allowed role must be selected if the microflow is used from navigation, a page, a nanoflow or a published service." Microflows called from test page buttons have no security roles configured. +- **roundtrip gaps**: + - Java actions round-trip correctly: basic (GetCurrentTimestamp), primitive params (ToUpperCase, Concatenate), entity params (SendEmail), list params (ProcessEmails), type parameters (`ENTITY `), `EXPOSED AS` (FormatCurrency) + - Inline Java code preserved with formatting (extra indentation added on read-back but functionally equivalent) + - Microflows calling java actions: parameter mappings, `ON ERROR ROLLBACK` default, entity type params (`EntityType = 'JaTest.EmailMessage'`) all correct +- **notes**: CE0106 is expected — the test script creates a page with buttons calling microflows without setting up module roles/access. Script-level issue, not mxcli bug. +- **status**: PASS (roundtrip correct, CE0106 expected for page-button microflows without roles) + +### 08-security-examples.mdl +- **exec**: OK — created module roles (User, Administrator, Viewer, Manager), user roles (RegularUser, SuperAdmin), demo users, grants/revokes all executed +- **mx check**: FAIL: 2 CE0463 errors — "The definition of this widget has changed" at Data grid 2 widgets (dgOrder, dgCustomer) +- **roundtrip gaps**: + - Module roles: 4 created with correct descriptions + - User roles: RegularUser (2 module roles), SuperAdmin (manage all) — PowerUser correctly dropped + - Demo users: sectest_user dropped, sectest_admin remains with roles (RegularUser, SuperAdmin) + - Microflow access: ACT_Customer_Create→User,Manager; ACT_Customer_Delete→Manager,Administrator; ACT_Order_Process→Administrator,Manager — matches final state after grants/revokes + - Page access: Customer_Overview→User,Manager,Administrator; Order_Overview→Administrator,Manager — correct + - Entity access: all revoked at end (cleanup section) — correct + - One minor gap: `REVOKE SecTest.Manager ON SecTest.Customer` prints "No access rules found" — Manager role was never granted entity access on Customer, so this is harmless +- **notes**: CE0463 on DATAGRID widgets is a known widget template issue, unrelated to security features. +- **status**: PARTIAL (security roundtrip correct; CE0463 from DATAGRID widget template is a separate known issue) + +### 11-navigation-examples.mdl +- **exec**: OK — created NavTest module, page, multiple CREATE OR REPLACE NAVIGATION statements, catalog queries +- **mx check**: PASS — 0 errors +- **roundtrip gaps**: + - Final navigation state matches last `CREATE OR REPLACE NAVIGATION` statement (HOME PAGE MyFirstModule.Home_Web, MENU with Home item) + - Intermediate navigation changes correctly overwritten by subsequent CREATE OR REPLACE + - LOGIN PAGE not shown in DESCRIBE output (may be omitted when using default, or Administration.Login is the default) + - Role-based HOME PAGE override (`FOR Administration.Administrator`) not visible in final DESCRIBE (overwritten by later CREATE OR REPLACE that didn't include it) + - SHOW NAVIGATION, SHOW NAVIGATION MENU, SHOW NAVIGATION HOMES all worked + - REFRESH CATALOG FULL and catalog queries executed correctly +- **status**: PASS + +### 13-business-events-examples.mdl +- **exec**: OK — created BusinessEvents stub module, entities, 2 services, CREATE OR REPLACE, DROP +- **mx check**: FAIL (expected): 1 error — "BusinessEvents module version folder not found" +- **roundtrip gaps**: + - CustomerEventsApi DESCRIBE matches input exactly (ServiceName, EventNamePrefix, 2 messages with PUBLISH + ENTITY binding) + - SimpleEvents correctly replaced via CREATE OR REPLACE (ServiceName changed to 'SimpleEventsUpdated', EventNamePrefix to 'v2') + - SimpleEvents correctly dropped via DROP BUSINESS EVENT SERVICE + - SHOW BUSINESS EVENT SERVICES and SHOW BUSINESS EVENTS output correct +- **notes**: CE error is expected — the test creates a stub BusinessEvents module, but mx expects the full marketplace module structure. +- **status**: PASS (roundtrip correct, CE error is environment-dependent) + +### 14-project-settings-examples.mdl +- **exec**: OK — all ALTER SETTINGS executed (MODEL, CONFIGURATION, CONSTANT, LANGUAGE, WORKFLOWS) +- **mx check**: PASS — 0 errors +- **roundtrip gaps**: + - MODEL settings verified: AfterStartupMicroflow='MyModule.ASU_Startup', HashAlgorithm='BCrypt', JavaVersion='Java21', RoundingMode='HalfUp', BcryptCost=12, AllowUserMultipleSessions=true — all match input + - CONFIGURATION 'Default' updated (DatabaseType, DatabaseUrl, etc.) + - CONSTANT override in configuration: MyModule.ServerUrl = 'kafka:9092' in 'Default' + - LANGUAGE DefaultLanguageCode = 'en_US' + - WORKFLOWS UserEntity = 'System.User' +- **status**: PASS + +### Summary + +| File | exec | mx check | roundtrip | status | +|------|------|----------|-----------|--------| +| 05-database-connection | OK | FAIL (4 CE - missing marketplace module) | Correct | PASS | +| 07-java-action | OK | FAIL (25 CE0106 - no security roles on page buttons) | Correct | PASS | +| 08-security | OK | FAIL (2 CE0463 - DATAGRID widget template) | Correct | PARTIAL | +| 11-navigation | OK | PASS (0 errors) | Correct | PASS | +| 13-business-events | OK | FAIL (1 CE - missing BusinessEvents module structure) | Correct | PASS | +| 14-project-settings | OK | PASS (0 errors) | Correct | PASS | + +**Key findings:** +1. All 6 files execute successfully — no exec errors +2. Roundtrip fidelity is excellent across all tested doc types +3. CE errors are all either environment-dependent (missing marketplace modules) or test-script-level issues (missing security roles for page-referenced microflows) +4. CE0463 on DATAGRID widgets (file 08) is a known widget template issue +5. No mxcli bugs found in these test files + +**Script re-run note:** Re-ran with `/tmp/mxrt/roundtrip.sh` — all 6 files exec=OK. The script's `describe-all.sh` only covers entities/microflows/pages (not database connections, java actions, security, navigation, business events, or project settings), so diff results are noisy and incomplete for these doc types. Manual DESCRIBE verification above is authoritative. diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index ae5134e..b9dc5e3 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -4,6 +4,7 @@ package executor import ( "fmt" + "log" "strings" "github.com/mendixlabs/mxcli/mdl/ast" @@ -32,6 +33,11 @@ type pageBuilder struct { fragments map[string]*ast.DefineFragmentStmt // Fragment registry from executor themeRegistry *ThemeRegistry // Theme design property definitions (may be nil) + // Pluggable widget engine (lazily initialized) + widgetRegistry *WidgetRegistry + pluggableEngine *PluggableWidgetEngine + pluggableEngineErr error // stores init failure reason for better error messages + // Per-operation caches (may change during execution) layoutsCache []*pages.Layout pagesCache []*pages.Page @@ -42,6 +48,26 @@ type pageBuilder struct { entityContext string // Qualified entity name (e.g., "Module.Entity") } +// initPluggableEngine lazily initializes the pluggable widget engine. +func (pb *pageBuilder) initPluggableEngine() { + if pb.pluggableEngine != nil || pb.pluggableEngineErr != nil { + return + } + registry, err := NewWidgetRegistry() + if err != nil { + pb.pluggableEngineErr = fmt.Errorf("widget registry init failed: %w", err) + log.Printf("warning: %v", pb.pluggableEngineErr) + return + } + if pb.reader != nil { + if loadErr := registry.LoadUserDefinitions(pb.reader.Path()); loadErr != nil { + log.Printf("warning: loading user widget definitions: %v", loadErr) + } + } + pb.widgetRegistry = registry + pb.pluggableEngine = NewPluggableWidgetEngine(NewOperationRegistry(), pb) +} + // registerWidgetName registers a widget name and returns an error if it's already used. // Widget names must be unique within a page/snippet. func (pb *pageBuilder) registerWidgetName(name string, id model.ID) error { diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index 7a24d41..fe89412 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -292,12 +292,8 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { return nil, fmt.Errorf("TABPAGE must be a direct child of TABCONTAINER") case "GROUPBOX": widget, err = pb.buildGroupBoxV3(w) - case "COMBOBOX": - widget, err = pb.buildComboBoxV3(w) case "RADIOBUTTONS": widget, err = pb.buildRadioButtonsV3(w) - case "GALLERY": - widget, err = pb.buildGalleryV3(w) case "NAVIGATIONLIST": widget, err = pb.buildNavigationListV3(w) case "ITEM": @@ -328,6 +324,16 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { case "DYNAMICIMAGE": widget, err = pb.buildDynamicImageV3(w) default: + // Try pluggable widget engine for registered widget types + pb.initPluggableEngine() + if pb.widgetRegistry != nil { + if def, ok := pb.widgetRegistry.Get(strings.ToUpper(w.Type)); ok { + return pb.pluggableEngine.Build(def, w) + } + } + if pb.pluggableEngineErr != nil { + return nil, fmt.Errorf("unsupported V3 widget type: %s (%v)", w.Type, pb.pluggableEngineErr) + } return nil, fmt.Errorf("unsupported V3 widget type: %s", w.Type) } diff --git a/mdl/executor/cmd_pages_builder_v3_pluggable.go b/mdl/executor/cmd_pages_builder_v3_pluggable.go index 44f72a5..d6cf8b6 100644 --- a/mdl/executor/cmd_pages_builder_v3_pluggable.go +++ b/mdl/executor/cmd_pages_builder_v3_pluggable.go @@ -19,284 +19,6 @@ import ( // Custom/Pluggable Widget Builders V3 // ============================================================================= -// buildComboBoxV3 creates a ComboBox CustomWidget from V3 syntax. -// Supports two modes: -// - Enumeration mode (default): COMBOBOX name (Attribute: EnumAttr) -// - Association mode: COMBOBOX name (Attribute: AssocName, DataSource: DATABASE FROM TargetEntity, CaptionAttribute: DisplayAttr) -// -// In association mode: -// - Attribute is the association name (e.g., Order_Customer) → sets attributeAssociation -// - CaptionAttribute is the display attribute on the target entity (e.g., Name) → sets optionsSourceAssociationCaptionAttribute -// - DataSource provides the selectable objects → sets optionsSourceAssociationDataSource -func (pb *pageBuilder) buildComboBoxV3(w *ast.WidgetV3) (*pages.CustomWidget, error) { - widgetID := model.ID(mpr.GenerateID()) - - // Load embedded template (required for pluggable widgets to work) - embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDComboBox, mpr.GenerateID, pb.reader.Path()) - if err != nil { - return nil, fmt.Errorf("failed to load ComboBox template: %w", err) - } - if embeddedType == nil || embeddedObject == nil { - return nil, fmt.Errorf("ComboBox template not found") - } - - // Convert widget IDs to pages.PropertyTypeIDEntry format - propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) - - updatedObject := embeddedObject - - // Check if DataSource is specified → association mode - if ds := w.GetDataSource(); ds != nil { - // ASSOCIATION MODE - // 1. Set optionsSourceType to "association" - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "optionsSourceType", func(val bson.D) bson.D { - return setPrimitiveValue(val, "association") - }) - - // 2. Build datasource to get the entity name for caption attribute resolution - dataSource, entityName, err := pb.buildDataSourceV3(ds) - if err != nil { - return nil, fmt.Errorf("failed to build ComboBox datasource: %w", err) - } - - // 3. Set attributeAssociation — association path + target entity - // MxBuild requires both: association path in AttributeRef (CE8812) and - // target entity in EntityRef (CE0642) - if attr := w.GetAttribute(); attr != "" { - assocPath := pb.resolveAssociationPath(attr) - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "attributeAssociation", func(val bson.D) bson.D { - return setAssociationRef(val, assocPath, entityName) - }) - } - - // 4. Set optionsSourceAssociationDataSource - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "optionsSourceAssociationDataSource", func(val bson.D) bson.D { - return setDataSource(val, dataSource) - }) - - // 5. Set optionsSourceAssociationCaptionAttribute — display attribute on target entity - if captionAttr := w.GetStringProp("CaptionAttribute"); captionAttr != "" { - var captionAttrPath string - if strings.Contains(captionAttr, ".") { - captionAttrPath = captionAttr - } else if entityName != "" { - captionAttrPath = entityName + "." + captionAttr - } else { - captionAttrPath = captionAttr - } - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "optionsSourceAssociationCaptionAttribute", func(val bson.D) bson.D { - return setAttributeRef(val, captionAttrPath) - }) - } - } else { - // ENUMERATION MODE (existing behavior) - if attr := w.GetAttribute(); attr != "" { - attrPath := pb.resolveAttributePath(attr) - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, "attributeEnumeration", func(val bson.D) bson.D { - return setAttributeRef(val, attrPath) - }) - } - } - - cb := &pages.CustomWidget{ - BaseWidget: pages.BaseWidget{ - BaseElement: model.BaseElement{ - ID: widgetID, - TypeName: "CustomWidgets$CustomWidget", - }, - Name: w.Name, - }, - Label: w.GetLabel(), - Editable: "Always", - RawType: embeddedType, - RawObject: updatedObject, - PropertyTypeIDMap: propertyTypeIDs, - ObjectTypeID: embeddedObjectTypeID, - } - - if err := pb.registerWidgetName(w.Name, cb.ID); err != nil { - return nil, err - } - - return cb, nil -} - -// buildGalleryV3 creates a Gallery widget from V3 syntax using the CustomWidget (pluggable widget) approach. -func (pb *pageBuilder) buildGalleryV3(w *ast.WidgetV3) (*pages.CustomWidget, error) { - widgetID := model.ID(mpr.GenerateID()) - - // Load embedded template (required for pluggable widgets to work) - embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDGallery, mpr.GenerateID, pb.reader.Path()) - if err != nil { - return nil, fmt.Errorf("failed to load Gallery template: %w", err) - } - if embeddedType == nil || embeddedObject == nil { - return nil, fmt.Errorf("Gallery template not found") - } - - // Convert widget IDs to pages.PropertyTypeIDEntry format - propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) - - // Build datasource from V3 DataSource property - var datasource pages.DataSource - if ds := w.GetDataSource(); ds != nil { - dataSource, entityName, err := pb.buildDataSourceV3(ds) - if err != nil { - return nil, fmt.Errorf("failed to build datasource: %w", err) - } - datasource = dataSource - if entityName != "" { - pb.entityContext = entityName - // Register widget name with entity for SELECTION datasource lookup - if w.Name != "" { - pb.paramEntityNames[w.Name] = entityName - } - } - } - - // Get selection mode (Single, Multiple, None) - selectionMode := w.GetSelection() - if selectionMode == "" { - selectionMode = "Single" // Default - } - - // Collect content widgets and filter widgets - var contentWidgets []bson.D - var filterWidgets []bson.D - - for _, child := range w.Children { - switch strings.ToUpper(child.Type) { - case "TEMPLATE": - // Template contains the content widgets - build each child - for _, templateChild := range child.Children { - widgetBSON, err := pb.buildWidgetV3ToBSON(templateChild) - if err != nil { - return nil, err - } - if widgetBSON != nil { - contentWidgets = append(contentWidgets, widgetBSON) - } - } - case "FILTER": - // Filter section contains filter widgets - for _, filterChild := range child.Children { - widgetBSON, err := pb.buildWidgetV3ToBSON(filterChild) - if err != nil { - return nil, err - } - if widgetBSON != nil { - filterWidgets = append(filterWidgets, widgetBSON) - } - } - default: - // Direct children become content - widgetBSON, err := pb.buildWidgetV3ToBSON(child) - if err != nil { - return nil, err - } - if widgetBSON != nil { - contentWidgets = append(contentWidgets, widgetBSON) - } - } - } - - // Update the template object with datasource, content, filters, and selection mode - updatedObject := pb.cloneGalleryObject(embeddedObject, propertyTypeIDs, datasource, contentWidgets, filterWidgets, selectionMode) - - gallery := &pages.CustomWidget{ - BaseWidget: pages.BaseWidget{ - BaseElement: model.BaseElement{ - ID: widgetID, - TypeName: "CustomWidgets$CustomWidget", - }, - Name: w.Name, - }, - Editable: "Always", - RawType: embeddedType, - RawObject: updatedObject, - PropertyTypeIDMap: propertyTypeIDs, - ObjectTypeID: embeddedObjectTypeID, - } - - if err := pb.registerWidgetName(w.Name, gallery.ID); err != nil { - return nil, err - } - - pb.entityContext = "" - return gallery, nil -} - -// cloneGalleryObject clones a Gallery template Object, updating datasource, content, filtersPlaceholder, and selection mode. -func (pb *pageBuilder) cloneGalleryObject(templateObject bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource, contentWidgets []bson.D, filterWidgets []bson.D, selectionMode string) bson.D { - result := make(bson.D, 0, len(templateObject)) - - for _, elem := range templateObject { - if elem.Key == "$ID" { - // Generate new ID for the object - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) - } else if elem.Key == "Properties" { - // Update datasource, content, filtersPlaceholder, and itemSelection properties - if propsArr, ok := elem.Value.(bson.A); ok { - updatedProps := pb.updateGalleryProperties(propsArr, propertyTypeIDs, datasource, contentWidgets, filterWidgets, selectionMode) - result = append(result, bson.E{Key: "Properties", Value: updatedProps}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -// updateGalleryProperties updates Gallery properties: datasource, content, filtersPlaceholder, and itemSelection. -func (pb *pageBuilder) updateGalleryProperties(props bson.A, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource, contentWidgets []bson.D, filterWidgets []bson.D, selectionMode string) bson.A { - result := bson.A{int32(2)} // Version marker - - // Get the property type IDs - datasourceEntry := propertyTypeIDs["datasource"] - contentEntry := propertyTypeIDs["content"] - filtersPlaceholderEntry := propertyTypeIDs["filtersPlaceholder"] - itemSelectionEntry := propertyTypeIDs["itemSelection"] - - for _, propVal := range props { - if _, ok := propVal.(int32); ok { - continue // Skip version markers - } - propMap, ok := propVal.(bson.D) - if !ok { - continue - } - - typePointer := pb.getTypePointerFromProperty(propMap) - if typePointer == datasourceEntry.PropertyTypeID && datasource != nil { - // Replace datasource - result = append(result, pb.buildGalleryDatasourceProperty(datasourceEntry, datasource)) - } else if typePointer == contentEntry.PropertyTypeID && len(contentWidgets) > 0 { - // Replace content widgets - result = append(result, pb.buildGalleryContentProperty(contentEntry, contentWidgets)) - } else if typePointer == filtersPlaceholderEntry.PropertyTypeID && len(filterWidgets) > 0 { - // Replace filter widgets - result = append(result, pb.buildGalleryFiltersProperty(filtersPlaceholderEntry, filterWidgets)) - } else if typePointer == itemSelectionEntry.PropertyTypeID && selectionMode != "" { - // Update selection mode - result = append(result, pb.buildGallerySelectionProperty(propMap, selectionMode)) - } else { - // Keep as-is but with new IDs - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - } - - return result -} - -// buildGalleryDatasourceProperty builds the datasource property for Gallery. -func (pb *pageBuilder) buildGalleryDatasourceProperty(entry pages.PropertyTypeIDEntry, datasource pages.DataSource) bson.D { - // Use the same DataSource serialization as DataGrid2 - return pb.buildDataGrid2Property(entry, datasource, "", "") -} - // buildGallerySelectionProperty clones an itemSelection property and updates the Selection value. func (pb *pageBuilder) buildGallerySelectionProperty(propMap bson.D, selectionMode string) bson.D { result := make(bson.D, 0, len(propMap)) @@ -360,89 +82,6 @@ func (pb *pageBuilder) cloneActionWithNewID(actionMap bson.D) bson.D { return result } -// buildGalleryContentProperty builds the content property for Gallery (Widgets type). -func (pb *pageBuilder) buildGalleryContentProperty(entry pages.PropertyTypeIDEntry, contentWidgets []bson.D) bson.D { - // Build widgets array - widgetsArr := bson.A{int32(2)} - for _, w := range contentWidgets { - widgetsArr = append(widgetsArr, w) - } - - return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: widgetsArr}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -// buildGalleryFiltersProperty builds the filtersPlaceholder property for Gallery (Widgets type). -func (pb *pageBuilder) buildGalleryFiltersProperty(entry pages.PropertyTypeIDEntry, filterWidgets []bson.D) bson.D { - // Build widgets array - widgetsArr := bson.A{int32(2)} - for _, w := range filterWidgets { - widgetsArr = append(widgetsArr, w) - } - - return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: widgetsArr}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} // buildWidgetV3ToBSON builds a V3 widget and serializes it directly to BSON. func (pb *pageBuilder) buildWidgetV3ToBSON(w *ast.WidgetV3) (bson.D, error) { diff --git a/mdl/executor/cmd_pages_describe_parse.go b/mdl/executor/cmd_pages_describe_parse.go index 59573a9..6134970 100644 --- a/mdl/executor/cmd_pages_describe_parse.go +++ b/mdl/executor/cmd_pages_describe_parse.go @@ -142,10 +142,13 @@ func (e *Executor) parseRawWidget(w map[string]any) []rawWidget { widget.Caption = e.extractLabelText(w) widget.Content = e.extractCustomWidgetAttribute(w) widget.RenderMode = e.extractCustomWidgetType(w) // Store widget type in RenderMode - // For ComboBox, extract datasource and caption attribute for association mode + // For ComboBox, extract datasource and association attribute for association mode. + // In association mode the Attribute binding is stored as EntityRef (not AttributeRef), + // so we must use extractCustomWidgetPropertyAssociation instead of the generic scan. if widget.RenderMode == "COMBOBOX" { widget.DataSource = e.extractComboBoxDataSource(w) if widget.DataSource != nil { + widget.Content = e.extractCustomWidgetPropertyAssociation(w, "attributeAssociation") widget.CaptionAttribute = e.extractCustomWidgetPropertyAttributeRef(w, "optionsSourceAssociationCaptionAttribute") } } diff --git a/mdl/executor/cmd_pages_describe_pluggable.go b/mdl/executor/cmd_pages_describe_pluggable.go index 3de9649..751afe0 100644 --- a/mdl/executor/cmd_pages_describe_pluggable.go +++ b/mdl/executor/cmd_pages_describe_pluggable.go @@ -6,6 +6,40 @@ import ( "strings" ) +// buildPropertyTypeKeyMap builds a map from PropertyType $ID to PropertyKey for a CustomWidget. +// This resolves TypePointer references in Object.Properties back to their property names. +// If withFallback is true, also checks widgetType["PropertyTypes"] directly (for widgets like +// Gallery/DataGrid2 that may store PropertyTypes at different nesting levels). +func buildPropertyTypeKeyMap(w map[string]any, withFallback bool) map[string]string { + propTypeKeyMap := make(map[string]string) + widgetType, ok := w["Type"].(map[string]any) + if !ok { + return propTypeKeyMap + } + var propTypes []any + if objType, ok := widgetType["ObjectType"].(map[string]any); ok { + propTypes = getBsonArrayElements(objType["PropertyTypes"]) + } + if withFallback && len(propTypes) == 0 { + propTypes = getBsonArrayElements(widgetType["PropertyTypes"]) + } + for _, pt := range propTypes { + ptMap, ok := pt.(map[string]any) + if !ok { + continue + } + key := extractString(ptMap["PropertyKey"]) + if key == "" { + continue + } + id := extractBinaryID(ptMap["$ID"]) + if id != "" { + propTypeKeyMap[id] = key + } + } + return propTypeKeyMap +} + // extractCustomWidgetAttribute extracts the attribute from a CustomWidget (e.g., ComboBox). func (e *Executor) extractCustomWidgetAttribute(w map[string]any) string { obj, ok := w["Object"].(map[string]any) @@ -86,28 +120,7 @@ func (e *Executor) extractComboBoxDataSource(w map[string]any) *rawDataSource { return nil } - // Build property key map from Type.ObjectType.PropertyTypes - propTypeKeyMap := make(map[string]string) - if widgetType, ok := w["Type"].(map[string]any); ok { - var propTypes []any - if objType, ok := widgetType["ObjectType"].(map[string]any); ok { - propTypes = getBsonArrayElements(objType["PropertyTypes"]) - } - for _, pt := range propTypes { - ptMap, ok := pt.(map[string]any) - if !ok { - continue - } - key := extractString(ptMap["PropertyKey"]) - if key == "" { - continue - } - id := extractBinaryID(ptMap["$ID"]) - if id != "" { - propTypeKeyMap[id] = key - } - } - } + propTypeKeyMap := buildPropertyTypeKeyMap(w, false) // Search through properties for optionsSourceAssociationDataSource props := getBsonArrayElements(obj["Properties"]) @@ -701,35 +714,8 @@ func (e *Executor) extractGalleryWidgetsByPropertyKey(w map[string]any, targetKe return nil } - // Build a map from PropertyType ID to PropertyKey - propTypeKeyMap := make(map[string]string) - if widgetType, ok := w["Type"].(map[string]any); ok { - // PropertyTypes are in ObjectType.PropertyTypes for Gallery/DataGrid2 - var propTypes []any - if objType, ok := widgetType["ObjectType"].(map[string]any); ok { - propTypes = getBsonArrayElements(objType["PropertyTypes"]) - } - // Fallback to direct PropertyTypes if not found in ObjectType - if len(propTypes) == 0 { - propTypes = getBsonArrayElements(widgetType["PropertyTypes"]) - } - for _, pt := range propTypes { - ptMap, ok := pt.(map[string]any) - if !ok { - continue - } - // Gallery uses "PropertyKey" field for the key name - key := extractString(ptMap["PropertyKey"]) - if key == "" { - continue - } - // Get the ID - can be string, binary, or map with $Subtype - id := extractBinaryID(ptMap["$ID"]) - if id != "" { - propTypeKeyMap[id] = key - } - } - } + // Build a map from PropertyType ID to PropertyKey (with fallback for Gallery/DataGrid2) + propTypeKeyMap := buildPropertyTypeKeyMap(w, true) // Search through properties for the named property props := getBsonArrayElements(obj["Properties"]) @@ -854,28 +840,7 @@ func (e *Executor) extractCustomWidgetPropertyAttributeRef(w map[string]any, pro return "" } - // Build property key map from Type.ObjectType.PropertyTypes - propTypeKeyMap := make(map[string]string) - if widgetType, ok := w["Type"].(map[string]any); ok { - var propTypes []any - if objType, ok := widgetType["ObjectType"].(map[string]any); ok { - propTypes = getBsonArrayElements(objType["PropertyTypes"]) - } - for _, pt := range propTypes { - ptMap, ok := pt.(map[string]any) - if !ok { - continue - } - key := extractString(ptMap["PropertyKey"]) - if key == "" { - continue - } - id := extractBinaryID(ptMap["$ID"]) - if id != "" { - propTypeKeyMap[id] = key - } - } - } + propTypeKeyMap := buildPropertyTypeKeyMap(w, false) // Search through properties for the named property props := getBsonArrayElements(obj["Properties"]) @@ -902,35 +867,63 @@ func (e *Executor) extractCustomWidgetPropertyAttributeRef(w map[string]any, pro return "" } -// extractCustomWidgetPropertyString extracts a string property value from a CustomWidget. -func (e *Executor) extractCustomWidgetPropertyString(w map[string]any, propertyKey string) string { +// extractCustomWidgetPropertyAssociation extracts an association name from a named +// CustomWidget property that was written by opAssociation (setAssociationRef). +// The association is stored as EntityRef.Steps[1].Association (qualified path); +// this function returns only the short name (last segment after the final dot). +// +// This is the symmetric counterpart of extractCustomWidgetPropertyAttributeRef, +// handling the EntityRef storage format instead of AttributeRef. +func (e *Executor) extractCustomWidgetPropertyAssociation(w map[string]any, propertyKey string) string { obj, ok := w["Object"].(map[string]any) if !ok { return "" } - // Build property key map from Type.ObjectType.PropertyTypes - propTypeKeyMap := make(map[string]string) - if widgetType, ok := w["Type"].(map[string]any); ok { - var propTypes []any - if objType, ok := widgetType["ObjectType"].(map[string]any); ok { - propTypes = getBsonArrayElements(objType["PropertyTypes"]) + propTypeKeyMap := buildPropertyTypeKeyMap(w, false) + + // Find the named property and extract EntityRef.Steps[1].Association + props := getBsonArrayElements(obj["Properties"]) + for _, prop := range props { + propMap, ok := prop.(map[string]any) + if !ok { + continue + } + typePointerID := extractBinaryID(propMap["TypePointer"]) + if propTypeKeyMap[typePointerID] != propertyKey { + continue } - for _, pt := range propTypes { - ptMap, ok := pt.(map[string]any) + value, ok := propMap["Value"].(map[string]any) + if !ok { + continue + } + entityRef, ok := value["EntityRef"].(map[string]any) + if !ok || entityRef == nil { + return "" + } + steps := getBsonArrayElements(entityRef["Steps"]) + // Steps layout: [int32(2), step0, step1, ...] — first element is version marker + for _, step := range steps { + stepMap, ok := step.(map[string]any) if !ok { continue } - key := extractString(ptMap["PropertyKey"]) - if key == "" { - continue - } - id := extractBinaryID(ptMap["$ID"]) - if id != "" { - propTypeKeyMap[id] = key + if assoc := extractString(stepMap["Association"]); assoc != "" { + return shortAttributeName(assoc) } } } + return "" +} + +// extractCustomWidgetPropertyString extracts a string property value from a CustomWidget. +func (e *Executor) extractCustomWidgetPropertyString(w map[string]any, propertyKey string) string { + obj, ok := w["Object"].(map[string]any) + if !ok { + return "" + } + + propTypeKeyMap := buildPropertyTypeKeyMap(w, false) // Search through properties for the named property props := getBsonArrayElements(obj["Properties"]) @@ -967,28 +960,7 @@ func (e *Executor) extractCustomWidgetPropertyAttributes(w map[string]any, prope return nil } - // Build property key map from Type.ObjectType.PropertyTypes - propTypeKeyMap := make(map[string]string) - if widgetType, ok := w["Type"].(map[string]any); ok { - var propTypes []any - if objType, ok := widgetType["ObjectType"].(map[string]any); ok { - propTypes = getBsonArrayElements(objType["PropertyTypes"]) - } - for _, pt := range propTypes { - ptMap, ok := pt.(map[string]any) - if !ok { - continue - } - key := extractString(ptMap["PropertyKey"]) - if key == "" { - continue - } - id := extractBinaryID(ptMap["$ID"]) - if id != "" { - propTypeKeyMap[id] = key - } - } - } + propTypeKeyMap := buildPropertyTypeKeyMap(w, false) // Search through properties for the named property props := getBsonArrayElements(obj["Properties"]) diff --git a/mdl/executor/cmd_pages_describe_pluggable_test.go b/mdl/executor/cmd_pages_describe_pluggable_test.go new file mode 100644 index 0000000..7656ca4 --- /dev/null +++ b/mdl/executor/cmd_pages_describe_pluggable_test.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Tests for Issue #21: ComboBox association Attribute written as pointer; DESCRIBE shows CaptionAttribute. +// +// Root cause: extractCustomWidgetAttribute blindly scans all properties for the first AttributeRef. +// In association mode, attributeAssociation uses EntityRef (not AttributeRef), so the scan +// skips it and finds optionsSourceAssociationCaptionAttribute's AttributeRef ("Name") instead. +// +// Fix: extractCustomWidgetPropertyAssociation — generic property-key-aware association extractor, +// symmetric to the existing extractCustomWidgetPropertyAttributeRef. +package executor + +import ( + "testing" +) + +// buildComboBoxAssocWidget builds a mock CustomWidget map matching the BSON structure +// written by the pluggable widget engine for COMBOBOX in association mode. +// TypePointer IDs are plain strings (extractBinaryID accepts strings directly). +func buildComboBoxAssocWidget(assocPath, captionAttrPath string) map[string]any { + const ( + idOptionsSourceType = "type-id-001" + idAttrAssociation = "type-id-002" + idAssocDataSource = "type-id-003" + idCaptionAttribute = "type-id-004" + ) + + widgetType := map[string]any{ + "WidgetId": "com.mendix.widget.web.combobox.Combobox", + "ObjectType": map[string]any{ + "PropertyTypes": []any{ + map[string]any{"$ID": idOptionsSourceType, "PropertyKey": "optionsSourceType"}, + map[string]any{"$ID": idAttrAssociation, "PropertyKey": "attributeAssociation"}, + map[string]any{"$ID": idAssocDataSource, "PropertyKey": "optionsSourceAssociationDataSource"}, + map[string]any{"$ID": idCaptionAttribute, "PropertyKey": "optionsSourceAssociationCaptionAttribute"}, + }, + }, + } + + // Properties mirror what setAssociationRef and setAttributeRef produce in the engine. + properties := []any{ + // optionsSourceType = "association" + map[string]any{ + "TypePointer": idOptionsSourceType, + "Value": map[string]any{"PrimitiveValue": "association"}, + }, + // attributeAssociation — uses EntityRef (written by opAssociation / setAssociationRef) + map[string]any{ + "TypePointer": idAttrAssociation, + "Value": map[string]any{ + "EntityRef": map[string]any{ + "$Type": "DomainModels$IndirectEntityRef", + "Steps": []any{ + int32(2), // version marker + map[string]any{ + "$Type": "DomainModels$EntityRefStep", + "Association": assocPath, + "DestinationEntity": "MyFirstModule.Category", + }, + }, + }, + }, + }, + // optionsSourceAssociationDataSource + map[string]any{ + "TypePointer": idAssocDataSource, + "Value": map[string]any{ + "DataSource": map[string]any{ + "$Type": "CustomWidgets$CustomWidgetXPathSource", + "EntityRef": map[string]any{"Entity": "MyFirstModule.Category"}, + }, + }, + }, + // optionsSourceAssociationCaptionAttribute — uses AttributeRef (written by opAttribute) + map[string]any{ + "TypePointer": idCaptionAttribute, + "Value": map[string]any{ + "AttributeRef": map[string]any{ + "$Type": "DomainModels$AttributeRef", + "Attribute": captionAttrPath, + }, + }, + }, + } + + return map[string]any{ + "Type": widgetType, + "Object": map[string]any{"Properties": properties}, + } +} + +// TestExtractCustomWidgetPropertyAssociation_ReturnsAssociationName verifies that +// the generic association extractor returns the short association name from the +// named property's EntityRef.Steps[1].Association. +// Regression test for Issue #21. +func TestExtractCustomWidgetPropertyAssociation_ReturnsAssociationName(t *testing.T) { + e := &Executor{} + w := buildComboBoxAssocWidget("MyFirstModule.Task_Category", "MyFirstModule.Category.Name") + + got := e.extractCustomWidgetPropertyAssociation(w, "attributeAssociation") + + if got != "Task_Category" { + t.Errorf("extractCustomWidgetPropertyAssociation(w, \"attributeAssociation\") = %q, want \"Task_Category\"", got) + } +} + +// TestExtractCustomWidgetPropertyAssociation_WrongKey returns empty for a non-matching key. +func TestExtractCustomWidgetPropertyAssociation_WrongKey(t *testing.T) { + e := &Executor{} + w := buildComboBoxAssocWidget("MyFirstModule.Task_Category", "MyFirstModule.Category.Name") + + got := e.extractCustomWidgetPropertyAssociation(w, "nonExistentProperty") + + if got != "" { + t.Errorf("extractCustomWidgetPropertyAssociation with wrong key = %q, want empty", got) + } +} + +// TestExtractCustomWidgetPropertyAssociation_NilEntityRef returns empty when EntityRef is nil. +func TestExtractCustomWidgetPropertyAssociation_NilEntityRef(t *testing.T) { + e := &Executor{} + w := map[string]any{ + "Type": map[string]any{ + "ObjectType": map[string]any{ + "PropertyTypes": []any{ + map[string]any{"$ID": "id-1", "PropertyKey": "attributeAssociation"}, + }, + }, + }, + "Object": map[string]any{ + "Properties": []any{ + map[string]any{ + "TypePointer": "id-1", + "Value": map[string]any{"EntityRef": nil}, + }, + }, + }, + } + + got := e.extractCustomWidgetPropertyAssociation(w, "attributeAssociation") + + if got != "" { + t.Errorf("extractCustomWidgetPropertyAssociation with nil EntityRef = %q, want empty", got) + } +} + +// TestExtractCustomWidgetPropertyAssociation_DoesNotReturnCaptionAttribute confirms that +// the fixer does not accidentally return the CaptionAttribute value. +// This documents the exact bug: the old generic scan returned "Name" (CaptionAttribute) +// because it found the first AttributeRef, which belonged to CaptionAttribute, not Attribute. +func TestExtractCustomWidgetPropertyAssociation_DoesNotReturnCaptionAttribute(t *testing.T) { + e := &Executor{} + w := buildComboBoxAssocWidget("MyFirstModule.Task_Category", "MyFirstModule.Category.Name") + + got := e.extractCustomWidgetPropertyAssociation(w, "attributeAssociation") + + if got == "Name" { + t.Errorf("extractCustomWidgetPropertyAssociation returned CaptionAttribute value %q; this is the original bug (Issue #21)", got) + } +} diff --git a/mdl/executor/widget_engine.go b/mdl/executor/widget_engine.go new file mode 100644 index 0000000..6abc3c1 --- /dev/null +++ b/mdl/executor/widget_engine.go @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "log" + "strings" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/widgets" + "go.mongodb.org/mongo-driver/bson" +) + +// defaultSlotContainer is the MDLContainer name that receives default (non-containerized) child widgets. +const defaultSlotContainer = "TEMPLATE" + +// ============================================================================= +// Pluggable Widget Engine — Core Types and Operation Registry +// ============================================================================= + +// WidgetDefinition describes how to construct a pluggable widget from MDL syntax. +// Loaded from embedded JSON definition files (*.def.json). +type WidgetDefinition struct { + WidgetID string `json:"widgetId"` + MDLName string `json:"mdlName"` + TemplateFile string `json:"templateFile"` + DefaultEditable string `json:"defaultEditable"` + PropertyMappings []PropertyMapping `json:"propertyMappings,omitempty"` + ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"` + Modes []WidgetMode `json:"modes,omitempty"` +} + +// WidgetMode defines a conditional configuration variant for a widget. +// For example, ComboBox has "enumeration" and "association" modes. +// Modes are evaluated in order; the first matching condition wins. +// A mode with no condition acts as the default fallback. +type WidgetMode struct { + Name string `json:"name,omitempty"` + Condition string `json:"condition,omitempty"` + Description string `json:"description,omitempty"` + PropertyMappings []PropertyMapping `json:"propertyMappings"` + ChildSlots []ChildSlotMapping `json:"childSlots,omitempty"` +} + +// PropertyMapping maps an MDL source (attribute, association, literal, etc.) +// to a pluggable widget property key via a named operation. +type PropertyMapping struct { + PropertyKey string `json:"propertyKey"` + Source string `json:"source,omitempty"` + Value string `json:"value,omitempty"` + Operation string `json:"operation"` + Default string `json:"default,omitempty"` +} + +// ChildSlotMapping maps an MDL child container (e.g., TEMPLATE, FILTER) to a +// widget property that holds child widgets. +type ChildSlotMapping struct { + PropertyKey string `json:"propertyKey"` + MDLContainer string `json:"mdlContainer"` + Operation string `json:"operation"` +} + +// BuildContext carries resolved values from MDL parsing for use by operations. +type BuildContext struct { + AttributePath string + AssocPath string + EntityName string + PrimitiveVal string + DataSource pages.DataSource + ChildWidgets []bson.D +} + +// OperationFunc updates a template object's property identified by propertyKey. +// It receives the current object BSON, the property type ID map, the target key, +// and the build context containing resolved values. +type OperationFunc func(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D + +// OperationRegistry maps operation names to their implementations. +type OperationRegistry struct { + operations map[string]OperationFunc +} + +// NewOperationRegistry creates a registry pre-loaded with the 6 built-in operations. +func NewOperationRegistry() *OperationRegistry { + reg := &OperationRegistry{ + operations: make(map[string]OperationFunc), + } + reg.Register("attribute", opAttribute) + reg.Register("association", opAssociation) + reg.Register("primitive", opPrimitive) + reg.Register("selection", opSelection) + reg.Register("datasource", opDatasource) + reg.Register("widgets", opWidgets) + return reg +} + +// Register adds or replaces an operation by name. +func (r *OperationRegistry) Register(name string, fn OperationFunc) { + r.operations[name] = fn +} + +// Lookup returns the operation function for the given name, or nil if not found. +func (r *OperationRegistry) Lookup(name string) OperationFunc { + return r.operations[name] +} + +// Has returns true if the named operation is registered. +func (r *OperationRegistry) Has(name string) bool { + _, ok := r.operations[name] + return ok +} + +// ============================================================================= +// Built-in Operations +// ============================================================================= + +// opAttribute sets an attribute reference on a widget property. +func opAttribute(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if ctx.AttributePath == "" { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + return setAttributeRef(val, ctx.AttributePath) + }) +} + +// opAssociation sets an association reference (AttributeRef + EntityRef) on a widget property. +func opAssociation(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if ctx.AssocPath == "" { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + return setAssociationRef(val, ctx.AssocPath, ctx.EntityName) + }) +} + +// opPrimitive sets a primitive string value on a widget property. +func opPrimitive(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if ctx.PrimitiveVal == "" { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + return setPrimitiveValue(val, ctx.PrimitiveVal) + }) +} + +// opSelection sets a selection mode on a widget property, updating the Selection field +// inside the WidgetValue (which requires a deeper update than opPrimitive's PrimitiveValue). +func opSelection(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if ctx.PrimitiveVal == "" { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Selection" { + result = append(result, bson.E{Key: "Selection", Value: ctx.PrimitiveVal}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +// opDatasource sets a data source on a widget property. +func opDatasource(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if ctx.DataSource == nil { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + return setDataSource(val, ctx.DataSource) + }) +} + +// opWidgets replaces the Widgets array in a widget property value with child widgets. +func opWidgets(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + if len(ctx.ChildWidgets) == 0 { + return obj + } + return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { + return setChildWidgets(val, ctx.ChildWidgets) + }) +} + +// setChildWidgets replaces the Widgets field in a WidgetValue with the given child widgets. +func setChildWidgets(val bson.D, childWidgets []bson.D) bson.D { + widgetsArr := bson.A{int32(2)} // version marker + for _, w := range childWidgets { + widgetsArr = append(widgetsArr, w) + } + + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Widgets" { + result = append(result, bson.E{Key: "Widgets", Value: widgetsArr}) + } else { + result = append(result, elem) + } + } + return result +} + +// ============================================================================= +// Pluggable Widget Engine +// ============================================================================= + +// PluggableWidgetEngine builds CustomWidget instances from WidgetDefinition + AST. +type PluggableWidgetEngine struct { + operations *OperationRegistry + pageBuilder *pageBuilder +} + +// NewPluggableWidgetEngine creates a new engine with the given registry and page builder. +func NewPluggableWidgetEngine(ops *OperationRegistry, pb *pageBuilder) *PluggableWidgetEngine { + return &PluggableWidgetEngine{ + operations: ops, + pageBuilder: pb, + } +} + +// Build constructs a CustomWidget from a definition and AST widget node. +func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (*pages.CustomWidget, error) { + // Save and restore entity context (DataSource mappings may change it) + oldEntityContext := e.pageBuilder.entityContext + defer func() { e.pageBuilder.entityContext = oldEntityContext }() + + // 1. Load template + embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := + widgets.GetTemplateFullBSON(def.WidgetID, mpr.GenerateID, e.pageBuilder.reader.Path()) + if err != nil { + return nil, fmt.Errorf("failed to load %s template: %w", def.MDLName, err) + } + if embeddedType == nil || embeddedObject == nil { + return nil, fmt.Errorf("%s template not found", def.MDLName) + } + + propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) + updatedObject := embeddedObject + + // 2. Select mode and get mappings/slots + mappings, slots, err := e.selectMappings(def, w) + if err != nil { + return nil, err + } + + // 3. Apply property mappings + for _, mapping := range mappings { + ctx, err := e.resolveMapping(mapping, w) + if err != nil { + return nil, fmt.Errorf("failed to resolve mapping for %s: %w", mapping.PropertyKey, err) + } + + op := e.operations.Lookup(mapping.Operation) + if op == nil { + return nil, fmt.Errorf("unknown operation %q for property %s", mapping.Operation, mapping.PropertyKey) + } + + updatedObject = op(updatedObject, propertyTypeIDs, mapping.PropertyKey, ctx) + } + + // 4. Apply child slots + if err := e.applyChildSlots(slots, w, propertyTypeIDs, &updatedObject); err != nil { + return nil, err + } + + // 5. Build CustomWidget + widgetID := model.ID(mpr.GenerateID()) + cw := &pages.CustomWidget{ + BaseWidget: pages.BaseWidget{ + BaseElement: model.BaseElement{ + ID: widgetID, + TypeName: "CustomWidgets$CustomWidget", + }, + Name: w.Name, + }, + Label: w.GetLabel(), + Editable: def.DefaultEditable, + RawType: embeddedType, + RawObject: updatedObject, + PropertyTypeIDMap: propertyTypeIDs, + ObjectTypeID: embeddedObjectTypeID, + } + + if err := e.pageBuilder.registerWidgetName(w.Name, cw.ID); err != nil { + return nil, err + } + + return cw, nil +} + +// selectMappings selects the active PropertyMappings and ChildSlotMappings based on mode. +// Modes are evaluated in definition order; the first matching condition wins. +// A mode with no condition acts as the default fallback. +func (e *PluggableWidgetEngine) selectMappings(def *WidgetDefinition, w *ast.WidgetV3) ([]PropertyMapping, []ChildSlotMapping, error) { + // No modes defined — use top-level mappings directly + if len(def.Modes) == 0 { + return def.PropertyMappings, def.ChildSlots, nil + } + + // Evaluate modes in order; first match wins + var fallback *WidgetMode + for i := range def.Modes { + mode := &def.Modes[i] + if mode.Condition == "" { + // No condition = default fallback (use first one if multiple) + if fallback == nil { + fallback = mode + } + continue + } + if e.evaluateCondition(mode.Condition, w) { + return mode.PropertyMappings, mode.ChildSlots, nil + } + } + + // Use fallback mode + if fallback != nil { + return fallback.PropertyMappings, fallback.ChildSlots, nil + } + + return nil, nil, fmt.Errorf("no matching mode for widget %s", def.MDLName) +} + +// evaluateCondition checks a built-in condition string against the AST widget. +func (e *PluggableWidgetEngine) evaluateCondition(condition string, w *ast.WidgetV3) bool { + switch { + case condition == "hasDataSource": + return w.GetDataSource() != nil + case condition == "hasAttribute": + return w.GetAttribute() != "" + case strings.HasPrefix(condition, "hasProp:"): + propName := strings.TrimPrefix(condition, "hasProp:") + return w.GetStringProp(propName) != "" + default: + log.Printf("warning: unknown widget condition %q — returning false", condition) + return false + } +} + +// resolveMapping resolves a PropertyMapping's source into a BuildContext. +func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.WidgetV3) (*BuildContext, error) { + ctx := &BuildContext{} + + // Static value takes priority + if mapping.Value != "" { + ctx.PrimitiveVal = mapping.Value + return ctx, nil + } + + source := mapping.Source + if source == "" { + return ctx, nil + } + + switch source { + case "Attribute": + if attr := w.GetAttribute(); attr != "" { + ctx.AttributePath = e.pageBuilder.resolveAttributePath(attr) + } + + case "DataSource": + if ds := w.GetDataSource(); ds != nil { + dataSource, entityName, err := e.pageBuilder.buildDataSourceV3(ds) + if err != nil { + return nil, fmt.Errorf("failed to build datasource: %w", err) + } + ctx.DataSource = dataSource + ctx.EntityName = entityName + if entityName != "" { + e.pageBuilder.entityContext = entityName + if w.Name != "" { + e.pageBuilder.paramEntityNames[w.Name] = entityName + } + } + } + + case "Selection": + val := w.GetSelection() + if val == "" && mapping.Default != "" { + val = mapping.Default + } + ctx.PrimitiveVal = val + + case "CaptionAttribute": + if captionAttr := w.GetStringProp("CaptionAttribute"); captionAttr != "" { + // Resolve relative to entity context + if !strings.Contains(captionAttr, ".") && e.pageBuilder.entityContext != "" { + captionAttr = e.pageBuilder.entityContext + "." + captionAttr + } + ctx.AttributePath = captionAttr + } + + case "Association": + // For association operation: resolve both assoc path AND entity name from DataSource + if attr := w.GetAttribute(); attr != "" { + ctx.AssocPath = e.pageBuilder.resolveAssociationPath(attr) + } + // Entity name comes from DataSource context (must be resolved first by a DataSource mapping) + ctx.EntityName = e.pageBuilder.entityContext + + default: + // Generic fallback: treat source as a property name on the AST widget + val := w.GetStringProp(source) + if val == "" && mapping.Default != "" { + val = mapping.Default + } + ctx.PrimitiveVal = val + } + + return ctx, nil +} + +// applyChildSlots processes child slot mappings, building child widgets and embedding them. +func (e *PluggableWidgetEngine) applyChildSlots(slots []ChildSlotMapping, w *ast.WidgetV3, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, updatedObject *bson.D) error { + if len(slots) == 0 { + return nil + } + + // Build a set of slot container names for matching + slotContainers := make(map[string]*ChildSlotMapping, len(slots)) + for i := range slots { + slotContainers[slots[i].MDLContainer] = &slots[i] + } + + // Group children by slot + slotWidgets := make(map[string][]bson.D) + var defaultWidgets []bson.D + + for _, child := range w.Children { + upperType := strings.ToUpper(child.Type) + if slot, ok := slotContainers[upperType]; ok { + // Container matches a slot — build its children + for _, slotChild := range child.Children { + widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(slotChild) + if err != nil { + return err + } + if widgetBSON != nil { + slotWidgets[slot.PropertyKey] = append(slotWidgets[slot.PropertyKey], widgetBSON) + } + } + } else { + // Direct child — default content + widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(child) + if err != nil { + return err + } + if widgetBSON != nil { + defaultWidgets = append(defaultWidgets, widgetBSON) + } + } + } + + // Apply each slot's widgets via its operation + for _, slot := range slots { + childBSONs := slotWidgets[slot.PropertyKey] + // If no explicit container children, use default widgets for the first slot + if len(childBSONs) == 0 && len(defaultWidgets) > 0 && slot.MDLContainer == defaultSlotContainer { + childBSONs = defaultWidgets + defaultWidgets = nil // consume once + } + if len(childBSONs) == 0 { + continue + } + + op := e.operations.Lookup(slot.Operation) + if op == nil { + return fmt.Errorf("unknown operation %q for child slot %s", slot.Operation, slot.PropertyKey) + } + + ctx := &BuildContext{ChildWidgets: childBSONs} + *updatedObject = op(*updatedObject, propertyTypeIDs, slot.PropertyKey, ctx) + } + + return nil +} diff --git a/mdl/executor/widget_engine_test.go b/mdl/executor/widget_engine_test.go new file mode 100644 index 0000000..ec022c1 --- /dev/null +++ b/mdl/executor/widget_engine_test.go @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "bytes" + "encoding/json" + "log" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" + "go.mongodb.org/mongo-driver/bson" +) + +func TestWidgetDefinitionJSONRoundTrip(t *testing.T) { + original := WidgetDefinition{ + WidgetID: "com.mendix.widget.web.combobox.Combobox", + MDLName: "COMBOBOX", + TemplateFile: "combobox.json", + DefaultEditable: "Always", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "attributeEnumeration", Source: "Attribute", Operation: "attribute"}, + {PropertyKey: "optionsSourceType", Value: "enumeration", Operation: "primitive"}, + }, + ChildSlots: []ChildSlotMapping{ + {PropertyKey: "content", MDLContainer: "TEMPLATE", Operation: "widgets"}, + }, + Modes: []WidgetMode{ + { + Name: "association", + Condition: "hasDataSource", + Description: "Association-based ComboBox with datasource", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "attributeAssociation", Source: "Attribute", Operation: "association"}, + {PropertyKey: "optionsSourceType", Value: "association", Operation: "primitive"}, + }, + ChildSlots: []ChildSlotMapping{ + {PropertyKey: "menuContent", MDLContainer: "MENU", Operation: "widgets"}, + }, + }, + }, + } + + encoded, err := json.Marshal(original) + if err != nil { + t.Fatalf("failed to marshal WidgetDefinition: %v", err) + } + + var decoded WidgetDefinition + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatalf("failed to unmarshal WidgetDefinition: %v", err) + } + + // Verify top-level fields + if decoded.WidgetID != original.WidgetID { + t.Errorf("WidgetID: got %q, want %q", decoded.WidgetID, original.WidgetID) + } + if decoded.MDLName != original.MDLName { + t.Errorf("MDLName: got %q, want %q", decoded.MDLName, original.MDLName) + } + if decoded.DefaultEditable != original.DefaultEditable { + t.Errorf("DefaultEditable: got %q, want %q", decoded.DefaultEditable, original.DefaultEditable) + } + + // Verify property mappings + if len(decoded.PropertyMappings) != len(original.PropertyMappings) { + t.Fatalf("PropertyMappings count: got %d, want %d", len(decoded.PropertyMappings), len(original.PropertyMappings)) + } + if decoded.PropertyMappings[0].Operation != "attribute" { + t.Errorf("PropertyMappings[0].Operation: got %q, want %q", decoded.PropertyMappings[0].Operation, "attribute") + } + + // Verify child slots + if len(decoded.ChildSlots) != 1 { + t.Fatalf("ChildSlots count: got %d, want 1", len(decoded.ChildSlots)) + } + if decoded.ChildSlots[0].MDLContainer != "TEMPLATE" { + t.Errorf("ChildSlots[0].MDLContainer: got %q, want %q", decoded.ChildSlots[0].MDLContainer, "TEMPLATE") + } + + // Verify modes + if len(decoded.Modes) != 1 { + t.Fatalf("Modes count: got %d, want 1", len(decoded.Modes)) + } + assocMode := decoded.Modes[0] + if assocMode.Name != "association" { + t.Errorf("Mode name: got %q, want %q", assocMode.Name, "association") + } + if assocMode.Condition != "hasDataSource" { + t.Errorf("Mode condition: got %q, want %q", assocMode.Condition, "hasDataSource") + } + if len(assocMode.PropertyMappings) != 2 { + t.Errorf("Mode PropertyMappings count: got %d, want 2", len(assocMode.PropertyMappings)) + } + if len(assocMode.ChildSlots) != 1 { + t.Errorf("Mode ChildSlots count: got %d, want 1", len(assocMode.ChildSlots)) + } +} + +func TestWidgetDefinitionJSONOmitsEmptyOptionalFields(t *testing.T) { + minimal := WidgetDefinition{ + WidgetID: "com.example.Widget", + MDLName: "MYWIDGET", + TemplateFile: "mywidget.json", + DefaultEditable: "Always", + } + + encoded, err := json.Marshal(minimal) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + var raw map[string]json.RawMessage + if err := json.Unmarshal(encoded, &raw); err != nil { + t.Fatalf("failed to unmarshal to map: %v", err) + } + + // Verify that empty optional fields are omitted from JSON + omittedFields := []string{"propertyMappings", "childSlots", "modes"} + for _, field := range omittedFields { + if _, exists := raw[field]; exists { + t.Errorf("expected field %q to be omitted when empty, but it was present", field) + } + } + + // Verify required fields are present + requiredFields := []string{"widgetId", "mdlName", "templateFile", "defaultEditable"} + for _, field := range requiredFields { + if _, exists := raw[field]; !exists { + t.Errorf("expected required field %q to be present, but it was missing", field) + } + } +} + +func TestOperationRegistryLookupFound(t *testing.T) { + reg := NewOperationRegistry() + + builtinOps := []string{"attribute", "association", "primitive", "selection", "datasource", "widgets"} + for _, name := range builtinOps { + fn := reg.Lookup(name) + if fn == nil { + t.Errorf("Lookup(%q) returned nil, want non-nil", name) + } + } +} + +func TestOperationRegistryLookupNotFound(t *testing.T) { + reg := NewOperationRegistry() + + fn := reg.Lookup("nonexistent") + if fn != nil { + t.Error("Lookup(\"nonexistent\") should return nil") + } +} + +func TestOperationRegistryCustomRegistration(t *testing.T) { + reg := NewOperationRegistry() + + called := false + reg.Register("custom", func(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { + called = true + return obj + }) + + fn := reg.Lookup("custom") + if fn == nil { + t.Fatal("Lookup(\"custom\") returned nil after Register") + } + + fn(bson.D{}, nil, "test", &BuildContext{}) + if !called { + t.Error("custom operation was not called") + } +} + +// ============================================================================= +// PluggableWidgetEngine Tests +// ============================================================================= + +func TestEvaluateCondition(t *testing.T) { + engine := &PluggableWidgetEngine{ + operations: NewOperationRegistry(), + } + + tests := []struct { + name string + condition string + widget *ast.WidgetV3 + expected bool + }{ + { + name: "hasDataSource with datasource present", + condition: "hasDataSource", + widget: &ast.WidgetV3{ + Properties: map[string]any{ + "DataSource": &ast.DataSourceV3{Type: "database", Reference: "Module.Entity"}, + }, + }, + expected: true, + }, + { + name: "hasDataSource without datasource", + condition: "hasDataSource", + widget: &ast.WidgetV3{Properties: map[string]any{}}, + expected: false, + }, + { + name: "hasAttribute with attribute present", + condition: "hasAttribute", + widget: &ast.WidgetV3{Properties: map[string]any{"Attribute": "Name"}}, + expected: true, + }, + { + name: "hasAttribute without attribute", + condition: "hasAttribute", + widget: &ast.WidgetV3{Properties: map[string]any{}}, + expected: false, + }, + { + name: "hasProp with matching prop", + condition: "hasProp:CaptionAttribute", + widget: &ast.WidgetV3{Properties: map[string]any{"CaptionAttribute": "DisplayName"}}, + expected: true, + }, + { + name: "hasProp without matching prop", + condition: "hasProp:CaptionAttribute", + widget: &ast.WidgetV3{Properties: map[string]any{}}, + expected: false, + }, + { + name: "unknown condition returns false", + condition: "unknownCondition", + widget: &ast.WidgetV3{Properties: map[string]any{}}, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := engine.evaluateCondition(tc.condition, tc.widget) + if result != tc.expected { + t.Errorf("evaluateCondition(%q) = %v, want %v", tc.condition, result, tc.expected) + } + }) + } +} + +func TestEvaluateConditionUnknownLogsWarning(t *testing.T) { + engine := &PluggableWidgetEngine{ + operations: NewOperationRegistry(), + } + + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(nil) + + w := &ast.WidgetV3{Properties: map[string]any{}} + result := engine.evaluateCondition("typoCondition", w) + + if result != false { + t.Errorf("expected false for unknown condition, got %v", result) + } + if !strings.Contains(buf.String(), "typoCondition") { + t.Errorf("expected warning log mentioning 'typoCondition', got: %q", buf.String()) + } +} + +func TestSelectMappings_NoModes(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + def := &WidgetDefinition{ + PropertyMappings: []PropertyMapping{ + {PropertyKey: "attr", Source: "Attribute", Operation: "attribute"}, + }, + ChildSlots: []ChildSlotMapping{ + {PropertyKey: "content", MDLContainer: "TEMPLATE", Operation: "widgets"}, + }, + } + w := &ast.WidgetV3{Properties: map[string]any{}} + + mappings, slots, err := engine.selectMappings(def, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(mappings) != 1 || mappings[0].PropertyKey != "attr" { + t.Errorf("expected 1 mapping with key 'attr', got %v", mappings) + } + if len(slots) != 1 || slots[0].PropertyKey != "content" { + t.Errorf("expected 1 slot with key 'content', got %v", slots) + } +} + +func TestSelectMappings_WithModes(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + def := &WidgetDefinition{ + Modes: []WidgetMode{ + { + Name: "association", + Condition: "hasDataSource", + PropertyMappings: []PropertyMapping{{PropertyKey: "assoc", Operation: "association"}}, + }, + { + Name: "default", + PropertyMappings: []PropertyMapping{{PropertyKey: "enum", Operation: "attribute"}}, + }, + }, + } + + t.Run("matches association mode", func(t *testing.T) { + w := &ast.WidgetV3{ + Properties: map[string]any{ + "DataSource": &ast.DataSourceV3{Type: "database"}, + }, + } + mappings, _, err := engine.selectMappings(def, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(mappings) != 1 || mappings[0].PropertyKey != "assoc" { + t.Errorf("expected association mode, got %v", mappings) + } + }) + + t.Run("falls back to default mode", func(t *testing.T) { + w := &ast.WidgetV3{Properties: map[string]any{}} + mappings, _, err := engine.selectMappings(def, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(mappings) != 1 || mappings[0].PropertyKey != "enum" { + t.Errorf("expected default mode, got %v", mappings) + } + }) +} + +func TestResolveMapping_StaticValue(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + mapping := PropertyMapping{ + PropertyKey: "optionsSourceType", + Value: "association", + Operation: "primitive", + } + w := &ast.WidgetV3{Properties: map[string]any{}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.PrimitiveVal != "association" { + t.Errorf("expected PrimitiveVal='association', got %q", ctx.PrimitiveVal) + } +} + +func TestResolveMapping_AttributeSource(t *testing.T) { + pb := &pageBuilder{ + entityContext: "Module.Entity", + paramEntityNames: map[string]string{}, + widgetScope: map[string]model.ID{}, + } + engine := &PluggableWidgetEngine{ + operations: NewOperationRegistry(), + pageBuilder: pb, + } + + mapping := PropertyMapping{ + PropertyKey: "attributeEnumeration", + Source: "Attribute", + Operation: "attribute", + } + w := &ast.WidgetV3{Properties: map[string]any{"Attribute": "Name"}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.AttributePath != "Module.Entity.Name" { + t.Errorf("expected AttributePath='Module.Entity.Name', got %q", ctx.AttributePath) + } +} + +func TestResolveMapping_SelectionWithDefault(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + mapping := PropertyMapping{ + PropertyKey: "itemSelection", + Source: "Selection", + Operation: "primitive", + Default: "Single", + } + + t.Run("uses AST value when present", func(t *testing.T) { + w := &ast.WidgetV3{Properties: map[string]any{"Selection": "Multiple"}} + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.PrimitiveVal != "Multiple" { + t.Errorf("expected PrimitiveVal='Multiple', got %q", ctx.PrimitiveVal) + } + }) + + t.Run("uses default when AST value empty", func(t *testing.T) { + w := &ast.WidgetV3{Properties: map[string]any{}} + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.PrimitiveVal != "Single" { + t.Errorf("expected PrimitiveVal='Single', got %q", ctx.PrimitiveVal) + } + }) +} + +func TestResolveMapping_GenericProp(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + mapping := PropertyMapping{ + PropertyKey: "customProp", + Source: "MyCustomProp", + Operation: "primitive", + } + w := &ast.WidgetV3{Properties: map[string]any{"MyCustomProp": "customValue"}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.PrimitiveVal != "customValue" { + t.Errorf("expected PrimitiveVal='customValue', got %q", ctx.PrimitiveVal) + } +} + +func TestResolveMapping_EmptySource(t *testing.T) { + engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + + mapping := PropertyMapping{ + PropertyKey: "someProp", + Operation: "primitive", + } + w := &ast.WidgetV3{Properties: map[string]any{}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.PrimitiveVal != "" || ctx.AttributePath != "" { + t.Errorf("expected empty context, got %+v", ctx) + } +} + +func TestResolveMapping_CaptionAttribute(t *testing.T) { + pb := &pageBuilder{ + entityContext: "Module.Customer", + paramEntityNames: map[string]string{}, + widgetScope: map[string]model.ID{}, + } + engine := &PluggableWidgetEngine{ + operations: NewOperationRegistry(), + pageBuilder: pb, + } + + mapping := PropertyMapping{ + PropertyKey: "captionAttr", + Source: "CaptionAttribute", + Operation: "attribute", + } + w := &ast.WidgetV3{Properties: map[string]any{"CaptionAttribute": "FullName"}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.AttributePath != "Module.Customer.FullName" { + t.Errorf("expected 'Module.Customer.FullName', got %q", ctx.AttributePath) + } +} + +func TestResolveMapping_Association(t *testing.T) { + pb := &pageBuilder{ + entityContext: "Module.Order", + paramEntityNames: map[string]string{}, + widgetScope: map[string]model.ID{}, + } + engine := &PluggableWidgetEngine{ + operations: NewOperationRegistry(), + pageBuilder: pb, + } + + mapping := PropertyMapping{ + PropertyKey: "attributeAssociation", + Source: "Association", + Operation: "association", + } + w := &ast.WidgetV3{Properties: map[string]any{"Attribute": "Order_Customer"}} + + ctx, err := engine.resolveMapping(mapping, w) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.AssocPath != "Module.Order_Customer" { + t.Errorf("expected AssocPath='Module.Order_Customer', got %q", ctx.AssocPath) + } + if ctx.EntityName != "Module.Order" { + t.Errorf("expected EntityName='Module.Order', got %q", ctx.EntityName) + } +} + +func TestSetChildWidgets(t *testing.T) { + val := bson.D{ + {Key: "PrimitiveValue", Value: ""}, + {Key: "Widgets", Value: bson.A{int32(2)}}, + {Key: "XPathConstraint", Value: ""}, + } + + childWidgets := []bson.D{ + {{Key: "$Type", Value: "Forms$TextBox"}, {Key: "Name", Value: "textBox1"}}, + {{Key: "$Type", Value: "Forms$TextBox"}, {Key: "Name", Value: "textBox2"}}, + } + + updated := setChildWidgets(val, childWidgets) + + // Find Widgets field + for _, elem := range updated { + if elem.Key == "Widgets" { + arr, ok := elem.Value.(bson.A) + if !ok { + t.Fatal("Widgets value is not bson.A") + } + // Should have version marker + 2 widgets + if len(arr) != 3 { + t.Errorf("Widgets array length: got %d, want 3", len(arr)) + } + // First element should be version marker + if marker, ok := arr[0].(int32); !ok || marker != 2 { + t.Errorf("Widgets[0]: got %v, want int32(2)", arr[0]) + } + return + } + } + t.Error("Widgets field not found in result") +} + +func TestOpSelection(t *testing.T) { + // Call the real opSelection function with a properly structured widget BSON. + typePointerBytes := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + typePointerUUID := mpr.BlobToUUID(typePointerBytes) + + widgetObj := bson.D{ + {Key: "Properties", Value: bson.A{ + int32(2), // version marker + bson.D{ + {Key: "TypePointer", Value: typePointerBytes}, + {Key: "Value", Value: bson.D{ + {Key: "PrimitiveValue", Value: ""}, + {Key: "Selection", Value: "None"}, + }}, + }, + }}, + } + + propTypeIDs := map[string]pages.PropertyTypeIDEntry{ + "selectionType": {PropertyTypeID: typePointerUUID}, + } + + ctx := &BuildContext{PrimitiveVal: "Multi"} + result := opSelection(widgetObj, propTypeIDs, "selectionType", ctx) + + // Extract the updated Value from Properties + var props bson.A + for _, elem := range result { + if elem.Key == "Properties" { + props = elem.Value.(bson.A) + } + } + prop := props[1].(bson.D) // skip version marker at index 0 + var val bson.D + for _, elem := range prop { + if elem.Key == "Value" { + val = elem.Value.(bson.D) + } + } + + selectionFound := false + for _, elem := range val { + if elem.Key == "Selection" { + selectionFound = true + if elem.Value != "Multi" { + t.Errorf("Selection: got %q, want %q", elem.Value, "Multi") + } + } + if elem.Key == "PrimitiveValue" { + if elem.Value != "" { + t.Errorf("PrimitiveValue should remain empty, got %q", elem.Value) + } + } + } + if !selectionFound { + t.Error("Selection field not found in result") + } +} + +func TestOpSelectionEmptyValue(t *testing.T) { + widgetObj := bson.D{ + {Key: "Properties", Value: bson.A{int32(2)}}, + } + ctx := &BuildContext{PrimitiveVal: ""} + result := opSelection(widgetObj, nil, "any", ctx) + + // With empty PrimitiveVal, opSelection returns obj unchanged + if len(result) != len(widgetObj) { + t.Errorf("expected unchanged obj, got different length: %d vs %d", len(result), len(widgetObj)) + } +} diff --git a/mdl/executor/widget_registry.go b/mdl/executor/widget_registry.go new file mode 100644 index 0000000..9969d10 --- /dev/null +++ b/mdl/executor/widget_registry.go @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/mendixlabs/mxcli/sdk/widgets/definitions" +) + +// WidgetRegistry holds loaded widget definitions keyed by uppercase MDL name. +type WidgetRegistry struct { + byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName + byWidgetID map[string]*WidgetDefinition // keyed by widgetId + opReg *OperationRegistry // used for validating definition operations +} + +// NewWidgetRegistry creates a registry pre-loaded with embedded definitions. +// Uses a default OperationRegistry for validation. Use NewWidgetRegistryWithOps +// to provide a custom registry with additional operations. +func NewWidgetRegistry() (*WidgetRegistry, error) { + return NewWidgetRegistryWithOps(NewOperationRegistry()) +} + +// NewWidgetRegistryWithOps creates a registry pre-loaded with embedded definitions, +// validating operations against the provided OperationRegistry. +func NewWidgetRegistryWithOps(opReg *OperationRegistry) (*WidgetRegistry, error) { + reg := &WidgetRegistry{ + byMDLName: make(map[string]*WidgetDefinition), + byWidgetID: make(map[string]*WidgetDefinition), + opReg: opReg, + } + + entries, err := definitions.EmbeddedFS.ReadDir(".") + if err != nil { + return nil, fmt.Errorf("read embedded definitions: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".def.json") { + continue + } + + data, err := definitions.EmbeddedFS.ReadFile(entry.Name()) + if err != nil { + return nil, fmt.Errorf("read definition %s: %w", entry.Name(), err) + } + + var def WidgetDefinition + if err := json.Unmarshal(data, &def); err != nil { + return nil, fmt.Errorf("parse definition %s: %w", entry.Name(), err) + } + + if err := validateDefinitionOperations(&def, entry.Name(), opReg); err != nil { + return nil, err + } + + reg.byMDLName[strings.ToUpper(def.MDLName)] = &def + reg.byWidgetID[def.WidgetID] = &def + } + + return reg, nil +} + +// Get returns a widget definition by MDL name (case-insensitive). +func (r *WidgetRegistry) Get(mdlName string) (*WidgetDefinition, bool) { + def, ok := r.byMDLName[strings.ToUpper(mdlName)] + return def, ok +} + +// GetByWidgetID returns a widget definition by its full widget ID. +func (r *WidgetRegistry) GetByWidgetID(widgetID string) (*WidgetDefinition, bool) { + def, ok := r.byWidgetID[widgetID] + return def, ok +} + +// All returns all registered definitions. +func (r *WidgetRegistry) All() []*WidgetDefinition { + result := make([]*WidgetDefinition, 0, len(r.byMDLName)) + for _, def := range r.byMDLName { + result = append(result, def) + } + return result +} + +// Count returns the number of registered definitions. +func (r *WidgetRegistry) Count() int { + return len(r.byMDLName) +} + +// LoadUserDefinitions scans global and project-level directories for user-provided definitions. +// Project definitions override global ones with the same MDL name. +func (r *WidgetRegistry) LoadUserDefinitions(projectPath string) error { + // 1. Global: ~/.mxcli/widgets/*.def.json + homeDir, err := os.UserHomeDir() + if err == nil { + globalDir := filepath.Join(homeDir, ".mxcli", "widgets") + if err := r.loadDefinitionsFromDir(globalDir); err != nil { + return fmt.Errorf("global widgets: %w", err) + } + } else { + log.Printf("warning: cannot determine home directory for user widget definitions: %v", err) + } + + // 2. Project: /.mxcli/widgets/*.def.json (overrides global) + if projectPath != "" { + projectDir := filepath.Dir(projectPath) + localDir := filepath.Join(projectDir, ".mxcli", "widgets") + if err := r.loadDefinitionsFromDir(localDir); err != nil { + return fmt.Errorf("project widgets: %w", err) + } + } + + return nil +} + +// loadDefinitionsFromDir loads all .def.json files from a directory. +// Returns nil if the directory doesn't exist; returns errors for malformed files. +func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + log.Printf("warning: cannot read widget definitions from %s: %v", dir, err) + return nil + } + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".def.json") { + continue + } + + filePath := filepath.Join(dir, entry.Name()) + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("read %s: %w", filePath, err) + } + + var def WidgetDefinition + if err := json.Unmarshal(data, &def); err != nil { + return fmt.Errorf("parse %s: %w", filePath, err) + } + + if def.WidgetID == "" || def.MDLName == "" { + return fmt.Errorf("invalid definition %s: widgetId and mdlName are required", entry.Name()) + } + + if err := validateDefinitionOperations(&def, entry.Name(), r.opReg); err != nil { + return err + } + + upperName := strings.ToUpper(def.MDLName) + if existing, ok := r.byMDLName[upperName]; ok { + log.Printf("info: user definition %q overrides built-in %s (widgetId: %s → %s)", + entry.Name(), def.MDLName, existing.WidgetID, def.WidgetID) + } + r.byMDLName[upperName] = &def + r.byWidgetID[def.WidgetID] = &def + } + return nil +} + +// validateDefinitionOperations checks that all operation names in a definition +// are recognized by the given OperationRegistry, and validates source/operation +// compatibility and mapping order dependencies. +func validateDefinitionOperations(def *WidgetDefinition, source string, opReg *OperationRegistry) error { + if err := validateMappings(def.PropertyMappings, source, "", opReg); err != nil { + return err + } + for _, s := range def.ChildSlots { + if !opReg.Has(s.Operation) { + return fmt.Errorf("%s: unknown operation %q in childSlots for key %q", source, s.Operation, s.PropertyKey) + } + } + for _, mode := range def.Modes { + ctx := fmt.Sprintf("mode %q ", mode.Name) + if err := validateMappings(mode.PropertyMappings, source, ctx, opReg); err != nil { + return err + } + for _, s := range mode.ChildSlots { + if !opReg.Has(s.Operation) { + return fmt.Errorf("%s: unknown operation %q in %schildSlots for key %q", source, s.Operation, ctx, s.PropertyKey) + } + } + } + return nil +} + +// sourceOperationCompatible checks that a mapping's Source and Operation are compatible. +var incompatibleSourceOps = map[string]map[string]bool{ + "Attribute": {"association": true, "datasource": true}, + "Association": {"attribute": true, "datasource": true}, + "DataSource": {"attribute": true, "association": true}, +} + +// validateMappings validates a slice of property mappings for operation existence, +// source/operation compatibility, and mapping order (Association requires prior DataSource). +func validateMappings(mappings []PropertyMapping, source, modeCtx string, opReg *OperationRegistry) error { + hasDataSource := false + for _, m := range mappings { + if !opReg.Has(m.Operation) { + return fmt.Errorf("%s: unknown operation %q in %spropertyMappings for key %q", source, m.Operation, modeCtx, m.PropertyKey) + } + // Check source/operation compatibility + if incompatible, ok := incompatibleSourceOps[m.Source]; ok { + if incompatible[m.Operation] { + return fmt.Errorf("%s: incompatible source %q with operation %q in %spropertyMappings for key %q", + source, m.Source, m.Operation, modeCtx, m.PropertyKey) + } + } + // Track DataSource ordering + if m.Source == "DataSource" { + hasDataSource = true + } + // Association depends on entityContext set by a prior DataSource mapping + if m.Source == "Association" && !hasDataSource { + return fmt.Errorf("%s: %spropertyMappings key %q uses source 'Association' before any 'DataSource' mapping — entityContext will be stale", + source, modeCtx, m.PropertyKey) + } + } + return nil +} diff --git a/mdl/executor/widget_registry_test.go b/mdl/executor/widget_registry_test.go new file mode 100644 index 0000000..c4fe530 --- /dev/null +++ b/mdl/executor/widget_registry_test.go @@ -0,0 +1,467 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "bytes" + "encoding/json" + "log" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/sdk/widgets/definitions" +) + +func TestRegistryLoadsAllEmbeddedDefinitions(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + // We expect 2 embedded definitions (combobox, gallery) + if got := reg.Count(); got != 2 { + t.Errorf("registry count = %d, want 2", got) + } +} + +func TestRegistryGetByMDLName(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + tests := []struct { + mdlName string + widgetID string + }{ + {"COMBOBOX", "com.mendix.widget.web.combobox.Combobox"}, + {"GALLERY", "com.mendix.widget.web.gallery.Gallery"}, + } + + for _, tt := range tests { + t.Run(tt.mdlName, func(t *testing.T) { + def, ok := reg.Get(tt.mdlName) + if !ok { + t.Fatalf("Get(%q) not found", tt.mdlName) + } + if def.WidgetID != tt.widgetID { + t.Errorf("WidgetID = %q, want %q", def.WidgetID, tt.widgetID) + } + }) + } +} + +func TestRegistryGetCaseInsensitive(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + // Should work with any case + for _, name := range []string{"combobox", "ComboBox", "COMBOBOX", "Combobox"} { + def, ok := reg.Get(name) + if !ok { + t.Errorf("Get(%q) not found", name) + continue + } + if def.MDLName != "COMBOBOX" { + t.Errorf("Get(%q).MDLName = %q, want COMBOBOX", name, def.MDLName) + } + } +} + +func TestRegistryGetUnknownWidget(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + _, ok := reg.Get("NONEXISTENT") + if ok { + t.Error("Get(NONEXISTENT) should return false") + } +} + +func TestRegistryGetByWidgetID(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + def, ok := reg.GetByWidgetID("com.mendix.widget.web.gallery.Gallery") + if !ok { + t.Fatal("GetByWidgetID(Gallery) not found") + } + if def.MDLName != "GALLERY" { + t.Errorf("MDLName = %q, want GALLERY", def.MDLName) + } +} + +func TestAllEmbeddedDefinitionsAreValidJSON(t *testing.T) { + entries, err := definitions.EmbeddedFS.ReadDir(".") + if err != nil { + t.Fatalf("ReadDir error: %v", err) + } + + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".def.json") { + continue + } + + t.Run(entry.Name(), func(t *testing.T) { + data, err := definitions.EmbeddedFS.ReadFile(entry.Name()) + if err != nil { + t.Fatalf("ReadFile error: %v", err) + } + + var def WidgetDefinition + if err := json.Unmarshal(data, &def); err != nil { + t.Fatalf("JSON unmarshal error: %v", err) + } + + // Validate required fields + if def.WidgetID == "" { + t.Error("widgetId is empty") + } + if def.MDLName == "" { + t.Error("mdlName is empty") + } + if def.TemplateFile == "" { + t.Error("templateFile is empty") + } + + // Must have either propertyMappings, modes, or childSlots + hasMappings := len(def.PropertyMappings) > 0 + hasModes := len(def.Modes) > 0 + hasSlots := len(def.ChildSlots) > 0 + if !hasMappings && !hasModes && !hasSlots { + t.Error("definition has no propertyMappings, modes, or childSlots") + } + }) + } +} + +func TestRegistryLoadUserDefinitions(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + // Create a temp directory with a custom definition + tmpDir := t.TempDir() + widgetsDir := filepath.Join(tmpDir, ".mxcli", "widgets") + if err := os.MkdirAll(widgetsDir, 0o755); err != nil { + t.Fatalf("MkdirAll error: %v", err) + } + + customDef := `{ + "widgetId": "com.example.custom.MyWidget", + "mdlName": "MYWIDGET", + "templateFile": "mywidget.json", + "defaultEditable": "Always", + "propertyMappings": [ + {"propertyKey": "value", "source": "Attribute", "operation": "attribute"} + ] + }` + + defPath := filepath.Join(widgetsDir, "mywidget.def.json") + if err := os.WriteFile(defPath, []byte(customDef), 0o644); err != nil { + t.Fatalf("WriteFile error: %v", err) + } + + // Create a fake project file in the temp directory + projectPath := filepath.Join(tmpDir, "App.mpr") + + // Load user definitions + if err := reg.LoadUserDefinitions(projectPath); err != nil { + t.Fatalf("LoadUserDefinitions error: %v", err) + } + + // The custom widget should now be found + def, ok := reg.Get("MYWIDGET") + if !ok { + t.Fatal("custom widget MYWIDGET not found after LoadUserDefinitions") + } + if def.WidgetID != "com.example.custom.MyWidget" { + t.Errorf("WidgetID = %q, want com.example.custom.MyWidget", def.WidgetID) + } + + // Built-in widgets should still be available + _, ok = reg.Get("COMBOBOX") + if !ok { + t.Error("built-in COMBOBOX lost after LoadUserDefinitions") + } +} + +func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) { + opReg := NewOperationRegistry() + + // Association before DataSource should fail validation + badDef := &WidgetDefinition{ + WidgetID: "com.example.Bad", + MDLName: "BAD", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "assocProp", Source: "Association", Operation: "association"}, + {PropertyKey: "dsProp", Source: "DataSource", Operation: "datasource"}, + }, + } + if err := validateDefinitionOperations(badDef, "bad.def.json", opReg); err == nil { + t.Error("expected error for Association before DataSource, got nil") + } + + // DataSource before Association should pass + goodDef := &WidgetDefinition{ + WidgetID: "com.example.Good", + MDLName: "GOOD", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "dsProp", Source: "DataSource", Operation: "datasource"}, + {PropertyKey: "assocProp", Source: "Association", Operation: "association"}, + }, + } + if err := validateDefinitionOperations(goodDef, "good.def.json", opReg); err != nil { + t.Errorf("unexpected error for DataSource before Association: %v", err) + } + + // Association in mode should also validate order + modeDef := &WidgetDefinition{ + WidgetID: "com.example.Mode", + MDLName: "MODE", + Modes: []WidgetMode{ + { + Name: "bad", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "assocProp", Source: "Association", Operation: "association"}, + {PropertyKey: "dsProp", Source: "DataSource", Operation: "datasource"}, + }, + }, + }, + } + if err := validateDefinitionOperations(modeDef, "mode.def.json", opReg); err == nil { + t.Error("expected error for Association before DataSource in mode, got nil") + } +} + +func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T) { + opReg := NewOperationRegistry() + + // Source "Attribute" with Operation "association" should fail + badDef := &WidgetDefinition{ + WidgetID: "com.example.Bad", + MDLName: "BAD", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "prop", Source: "Attribute", Operation: "association"}, + }, + } + if err := validateDefinitionOperations(badDef, "bad.def.json", opReg); err == nil { + t.Error("expected error for Source='Attribute' with Operation='association', got nil") + } + + // Source "Association" with Operation "attribute" should fail + badDef2 := &WidgetDefinition{ + WidgetID: "com.example.Bad2", + MDLName: "BAD2", + PropertyMappings: []PropertyMapping{ + {PropertyKey: "prop", Source: "Association", Operation: "attribute"}, + }, + } + if err := validateDefinitionOperations(badDef2, "bad2.def.json", opReg); err == nil { + t.Error("expected error for Source='Association' with Operation='attribute', got nil") + } +} + +func TestEmbeddedDefinitionsValidateRequiredFields(t *testing.T) { + // All embedded definitions must have non-empty WidgetID and MDLName + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + for _, def := range reg.All() { + if def.WidgetID == "" { + t.Errorf("embedded definition with MDLName=%q has empty WidgetID", def.MDLName) + } + if def.MDLName == "" { + t.Errorf("embedded definition with WidgetID=%q has empty MDLName", def.WidgetID) + } + } +} + +func TestRegistryUserDefinitionOverrideLogsWarning(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + // Create a user definition that overrides the built-in COMBOBOX + tmpDir := t.TempDir() + widgetsDir := filepath.Join(tmpDir, ".mxcli", "widgets") + if err := os.MkdirAll(widgetsDir, 0o755); err != nil { + t.Fatalf("MkdirAll error: %v", err) + } + + overrideDef := `{ + "widgetId": "com.mendix.widget.web.combobox.Combobox", + "mdlName": "COMBOBOX", + "templateFile": "combobox.json", + "defaultEditable": "Always", + "propertyMappings": [ + {"propertyKey": "value", "source": "Attribute", "operation": "attribute"} + ] + }` + + defPath := filepath.Join(widgetsDir, "combobox-override.def.json") + if err := os.WriteFile(defPath, []byte(overrideDef), 0o644); err != nil { + t.Fatalf("WriteFile error: %v", err) + } + + var buf bytes.Buffer + log.SetOutput(&buf) + defer log.SetOutput(nil) + + projectPath := filepath.Join(tmpDir, "App.mpr") + if err := reg.LoadUserDefinitions(projectPath); err != nil { + t.Fatalf("LoadUserDefinitions error: %v", err) + } + + if !strings.Contains(buf.String(), "COMBOBOX") { + t.Errorf("expected warning log about overriding COMBOBOX, got: %q", buf.String()) + } +} + +func TestRegistryComboboxModes(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + def, ok := reg.Get("COMBOBOX") + if !ok { + t.Fatal("COMBOBOX not found") + } + + if len(def.Modes) != 2 { + t.Fatalf("modes count = %d, want 2", len(def.Modes)) + } + + // First mode: association (conditional) + if def.Modes[0].Name != "association" { + t.Errorf("first mode name = %q, want association", def.Modes[0].Name) + } + if def.Modes[0].Condition != "hasDataSource" { + t.Errorf("association mode condition = %q, want hasDataSource", def.Modes[0].Condition) + } + if len(def.Modes[0].PropertyMappings) != 4 { + t.Errorf("association mode mappings = %d, want 4", len(def.Modes[0].PropertyMappings)) + } + + // Second mode: default (no condition) + if def.Modes[1].Name != "default" { + t.Errorf("second mode name = %q, want default", def.Modes[1].Name) + } + if len(def.Modes[1].PropertyMappings) != 1 { + t.Errorf("default mode mappings = %d, want 1", len(def.Modes[1].PropertyMappings)) + } +} + +func TestRegistryGalleryChildSlots(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + def, ok := reg.Get("GALLERY") + if !ok { + t.Fatal("GALLERY not found") + } + + if len(def.ChildSlots) != 3 { + t.Fatalf("childSlots count = %d, want 3", len(def.ChildSlots)) + } + + // Verify slot mappings + slotsByContainer := make(map[string]ChildSlotMapping) + for _, slot := range def.ChildSlots { + slotsByContainer[slot.MDLContainer] = slot + } + + contentSlot, ok := slotsByContainer["TEMPLATE"] + if !ok { + t.Fatal("TEMPLATE slot not found") + } + if contentSlot.PropertyKey != "content" { + t.Errorf("TEMPLATE slot propertyKey = %q, want content", contentSlot.PropertyKey) + } + + emptySlot, ok := slotsByContainer["EMPTYPLACEHOLDER"] + if !ok { + t.Fatal("EMPTYPLACEHOLDER slot not found") + } + if emptySlot.PropertyKey != "emptyPlaceholder" { + t.Errorf("EMPTYPLACEHOLDER slot propertyKey = %q, want emptyPlaceholder", emptySlot.PropertyKey) + } + + // FILTER must match what DESCRIBE outputs ("FILTER"), not the BSON property name + filterSlot, ok := slotsByContainer["FILTER"] + if !ok { + t.Fatal("FILTER slot not found — mdlContainer must be 'FILTER' to match DESCRIBE output") + } + if filterSlot.PropertyKey != "filtersPlaceholder" { + t.Errorf("FILTER slot propertyKey = %q, want filtersPlaceholder", filterSlot.PropertyKey) + } +} + +func TestGallerySelectionDefaultIsSingle(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + def, ok := reg.Get("GALLERY") + if !ok { + t.Fatal("GALLERY not found") + } + + // Find itemSelection mapping + for _, m := range def.PropertyMappings { + if m.PropertyKey == "itemSelection" { + if m.Default != "Single" { + t.Errorf("itemSelection default = %q, want %q", m.Default, "Single") + } + return + } + } + t.Fatal("itemSelection mapping not found in GALLERY definition") +} + +func TestComboboxAssociationModeUsesAssociationSource(t *testing.T) { + reg, err := NewWidgetRegistry() + if err != nil { + t.Fatalf("NewWidgetRegistry() error: %v", err) + } + + def, ok := reg.Get("COMBOBOX") + if !ok { + t.Fatal("COMBOBOX not found") + } + + // Find association mode + for _, mode := range def.Modes { + if mode.Name != "association" { + continue + } + for _, m := range mode.PropertyMappings { + if m.PropertyKey == "attributeAssociation" { + if m.Source != "Association" { + t.Errorf("attributeAssociation source = %q, want %q — 'Attribute' source populates AttributePath but opAssociation reads AssocPath", m.Source, "Association") + } + if m.Operation != "association" { + t.Errorf("attributeAssociation operation = %q, want %q", m.Operation, "association") + } + return + } + } + } + t.Fatal("attributeAssociation mapping not found in COMBOBOX association mode") +} diff --git a/sdk/widgets/augment.go b/sdk/widgets/augment.go index bcc47f4..5373d77 100644 --- a/sdk/widgets/augment.go +++ b/sdk/widgets/augment.go @@ -5,6 +5,7 @@ package widgets import ( "encoding/json" "fmt" + "sync/atomic" "github.com/mendixlabs/mxcli/sdk/widgets/mpk" ) @@ -224,9 +225,11 @@ func clonePropertyPair(propTypes []any, objProps []any, exemplarIdx int, p mpk.P vt["EnumerationValues"] = []any{float64(2)} } - // Clear ObjectType for non-object types + // Clear ObjectType for non-object types; build nested ObjectType for object types with children if vtType != "Object" { vt["ObjectType"] = nil + } else if len(p.Children) > 0 { + vt["ObjectType"] = buildNestedObjectType(p.Children) } // Clear ReturnType for non-expression types @@ -347,6 +350,11 @@ func createDefaultValueType(vtID string, bsonType string, p mpk.PropertyDef) map vt["DataSourceProperty"] = p.DataSource } + // Build nested ObjectType for object-type properties with children + if bsonType == "Object" && len(p.Children) > 0 { + vt["ObjectType"] = buildNestedObjectType(p.Children) + } + return vt } @@ -517,22 +525,56 @@ func xmlTypeToBSONType(xmlType string) string { } } +// buildNestedObjectType creates a WidgetObjectType with PropertyTypes for nested children +// of an object-type property. This is needed for properties like filterList and sortList +// that contain sub-properties (e.g., filter, attribute, caption). +func buildNestedObjectType(children []mpk.PropertyDef) map[string]any { + propTypes := []any{float64(2)} // version marker + + for _, child := range children { + childBsonType := xmlTypeToBSONType(child.Type) + if childBsonType == "" { + continue + } + + childVTID := placeholderID() + childPT := map[string]any{ + "$ID": placeholderID(), + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": child.Caption, + "Category": "General", + "Description": child.Description, + "IsDefault": false, + "PropertyKey": child.Key, + "ValueType": createDefaultValueType(childVTID, childBsonType, child), + } + + propTypes = append(propTypes, childPT) + } + + return map[string]any{ + "$ID": placeholderID(), + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": propTypes, + } +} + // --- Helpers --- -// placeholderCounter generates sequential placeholder IDs. -var placeholderCounter uint32 +// placeholderCounter generates sequential placeholder IDs (atomic for concurrent safety). +var placeholderCounter atomic.Uint32 // placeholderID generates a placeholder hex ID. These will be remapped by collectIDs // in GetTemplateFullBSON, so exact values don't matter — they just need to be unique // 32-char hex strings. func placeholderID() string { - placeholderCounter++ - return fmt.Sprintf("aa000000000000000000000000%06x", placeholderCounter) + n := placeholderCounter.Add(1) + return fmt.Sprintf("aa000000000000000000000000%06x", n) } // ResetPlaceholderCounter resets the counter (for testing). func ResetPlaceholderCounter() { - placeholderCounter = 0 + placeholderCounter.Store(0) } // getMapField gets a nested map field from a JSON map. diff --git a/sdk/widgets/definitions/combobox.def.json b/sdk/widgets/definitions/combobox.def.json new file mode 100644 index 0000000..21755ca --- /dev/null +++ b/sdk/widgets/definitions/combobox.def.json @@ -0,0 +1,26 @@ +{ + "widgetId": "com.mendix.widget.web.combobox.Combobox", + "mdlName": "COMBOBOX", + "templateFile": "combobox.json", + "defaultEditable": "Always", + "modes": [ + { + "name": "association", + "condition": "hasDataSource", + "description": "Association mode", + "propertyMappings": [ + {"propertyKey": "optionsSourceType", "value": "association", "operation": "primitive"}, + {"propertyKey": "optionsSourceAssociationDataSource", "source": "DataSource", "operation": "datasource"}, + {"propertyKey": "attributeAssociation", "source": "Association", "operation": "association"}, + {"propertyKey": "optionsSourceAssociationCaptionAttribute", "source": "CaptionAttribute", "operation": "attribute"} + ] + }, + { + "name": "default", + "description": "Enumeration mode", + "propertyMappings": [ + {"propertyKey": "attributeEnumeration", "source": "Attribute", "operation": "attribute"} + ] + } + ] +} diff --git a/sdk/widgets/definitions/gallery.def.json b/sdk/widgets/definitions/gallery.def.json new file mode 100644 index 0000000..d300c58 --- /dev/null +++ b/sdk/widgets/definitions/gallery.def.json @@ -0,0 +1,86 @@ +{ + "widgetId": "com.mendix.widget.web.gallery.Gallery", + "mdlName": "GALLERY", + "templateFile": "gallery.json", + "defaultEditable": "Always", + "propertyMappings": [ + { + "propertyKey": "advanced", + "value": "false", + "operation": "primitive" + }, + { + "propertyKey": "datasource", + "source": "DataSource", + "operation": "datasource" + }, + { + "propertyKey": "itemSelection", + "source": "Selection", + "operation": "selection", + "default": "Single" + }, + { + "propertyKey": "itemSelectionMode", + "value": "clear", + "operation": "primitive" + }, + { + "propertyKey": "desktopItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "tabletItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "phoneItems", + "value": "1", + "operation": "primitive" + }, + { + "propertyKey": "pageSize", + "value": "20", + "operation": "primitive" + }, + { + "propertyKey": "pagination", + "value": "buttons", + "operation": "primitive" + }, + { + "propertyKey": "pagingPosition", + "value": "below", + "operation": "primitive" + }, + { + "propertyKey": "showEmptyPlaceholder", + "value": "none", + "operation": "primitive" + }, + { + "propertyKey": "onClickTrigger", + "value": "single", + "operation": "primitive" + } + ], + "childSlots": [ + { + "propertyKey": "content", + "mdlContainer": "TEMPLATE", + "operation": "widgets" + }, + { + "propertyKey": "emptyPlaceholder", + "mdlContainer": "EMPTYPLACEHOLDER", + "operation": "widgets" + }, + { + "propertyKey": "filtersPlaceholder", + "mdlContainer": "FILTER", + "operation": "widgets" + } + ] +} diff --git a/sdk/widgets/definitions/loader.go b/sdk/widgets/definitions/loader.go new file mode 100644 index 0000000..ad6a716 --- /dev/null +++ b/sdk/widgets/definitions/loader.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package definitions provides embedded widget definition files for the pluggable widget engine. +package definitions + +import "embed" + +//go:embed *.def.json +var EmbeddedFS embed.FS diff --git a/sdk/widgets/mpk/mpk.go b/sdk/widgets/mpk/mpk.go index b1f40b0..066d0f9 100644 --- a/sdk/widgets/mpk/mpk.go +++ b/sdk/widgets/mpk/mpk.go @@ -17,16 +17,17 @@ import ( // PropertyDef describes a single property from a widget XML definition. type PropertyDef struct { - Key string // e.g. "staticDataSourceCaption" - Type string // XML type: "attribute", "expression", "textTemplate", "widgets", etc. + Key string // e.g. "staticDataSourceCaption" + Type string // XML type: "attribute", "expression", "textTemplate", "widgets", etc. Caption string Description string - Category string // from enclosing propertyGroup captions, joined with "::" + Category string // from enclosing propertyGroup captions, joined with "::" Required bool - DefaultValue string // for enumeration/boolean/integer types + DefaultValue string // for enumeration/boolean/integer types IsList bool - IsSystem bool // true for elements - DataSource string // dataSource attribute reference + IsSystem bool // true for elements + DataSource string // dataSource attribute reference + Children []PropertyDef // nested properties for object-type properties } // WidgetDefinition holds the parsed definition of a pluggable widget from an .mpk file. @@ -92,6 +93,12 @@ type xmlSystemProp struct { Key string `xml:"key,attr"` } +// Zip extraction limits to prevent zip-bomb attacks. +const ( + maxFileSize = 50 << 20 // 50MB per individual file + maxTotalSize = 200 << 20 // 200MB total extracted +) + // --- Caching --- var ( @@ -122,9 +129,13 @@ func ParseMPK(mpkPath string) (*WidgetDefinition, error) { var pkg xmlPackage var widgetFilePath string var version string + var totalExtracted uint64 for _, f := range r.File { if f.Name == "package.xml" { + if f.UncompressedSize64 > maxFileSize { + return nil, fmt.Errorf("package.xml exceeds max file size (%d > %d)", f.UncompressedSize64, maxFileSize) + } rc, err := f.Open() if err != nil { return nil, fmt.Errorf("failed to open package.xml: %w", err) @@ -134,6 +145,10 @@ func ParseMPK(mpkPath string) (*WidgetDefinition, error) { if err != nil { return nil, fmt.Errorf("failed to read package.xml: %w", err) } + totalExtracted += uint64(len(data)) + if totalExtracted > maxTotalSize { + return nil, fmt.Errorf("total extracted size exceeds limit (%d > %d)", totalExtracted, maxTotalSize) + } if err := xml.Unmarshal(data, &pkg); err != nil { return nil, fmt.Errorf("failed to parse package.xml: %w", err) } @@ -152,6 +167,9 @@ func ParseMPK(mpkPath string) (*WidgetDefinition, error) { // Parse widget XML for _, f := range r.File { if f.Name == widgetFilePath { + if f.UncompressedSize64 > maxFileSize { + return nil, fmt.Errorf("%s exceeds max file size (%d > %d)", widgetFilePath, f.UncompressedSize64, maxFileSize) + } rc, err := f.Open() if err != nil { return nil, fmt.Errorf("failed to open %s: %w", widgetFilePath, err) @@ -161,6 +179,10 @@ func ParseMPK(mpkPath string) (*WidgetDefinition, error) { if err != nil { return nil, fmt.Errorf("failed to read %s: %w", widgetFilePath, err) } + totalExtracted += uint64(len(data)) + if totalExtracted > maxTotalSize { + return nil, fmt.Errorf("total extracted size exceeds limit (%d > %d)", totalExtracted, maxTotalSize) + } var widget xmlWidget if err := xml.Unmarshal(data, &widget); err != nil { @@ -212,6 +234,14 @@ func walkPropertyGroup(pg xmlPropGroup, parentCategory string, def *WidgetDefini IsList: p.IsList == "true", DataSource: p.DataSource, } + + // Parse nested properties for object-type properties + if p.Type == "object" && len(p.NestedProps) > 0 { + for _, npg := range p.NestedProps { + collectNestedProperties(npg, &prop) + } + } + def.Properties = append(def.Properties, prop) } @@ -230,6 +260,28 @@ func walkPropertyGroup(pg xmlPropGroup, parentCategory string, def *WidgetDefini } } +// collectNestedProperties extracts child properties from nested propertyGroups +// within an object-type property and appends them to the parent PropertyDef. +func collectNestedProperties(pg xmlPropGroup, parent *PropertyDef) { + for _, p := range pg.Properties { + child := PropertyDef{ + Key: p.Key, + Type: p.Type, + Caption: p.Caption, + Description: p.Description, + Required: p.Required == "true", + DefaultValue: p.DefaultValue, + IsList: p.IsList == "true", + DataSource: p.DataSource, + } + parent.Children = append(parent.Children, child) + } + + for _, sub := range pg.SubGroups { + collectNestedProperties(sub, parent) + } +} + // FindMPK looks in the project's widgets/ directory for an .mpk matching the widgetID. // Returns the path to the .mpk file, or empty string if not found. func FindMPK(projectDir string, widgetID string) (string, error) { @@ -283,8 +335,12 @@ func getWidgetIDFromMPK(mpkPath string) (string, error) { // Find package.xml to get widget file path var widgetFilePath string + var totalExtracted uint64 for _, f := range r.File { if f.Name == "package.xml" { + if f.UncompressedSize64 > maxFileSize { + return "", fmt.Errorf("package.xml exceeds max file size (%d > %d)", f.UncompressedSize64, maxFileSize) + } rc, err := f.Open() if err != nil { return "", err @@ -294,6 +350,10 @@ func getWidgetIDFromMPK(mpkPath string) (string, error) { if err != nil { return "", err } + totalExtracted += uint64(len(data)) + if totalExtracted > maxTotalSize { + return "", fmt.Errorf("total extracted size exceeds limit (%d > %d)", totalExtracted, maxTotalSize) + } var pkg xmlPackage if err := xml.Unmarshal(data, &pkg); err != nil { return "", err @@ -312,6 +372,9 @@ func getWidgetIDFromMPK(mpkPath string) (string, error) { // Read widget XML to get the id attribute for _, f := range r.File { if f.Name == widgetFilePath { + if f.UncompressedSize64 > maxFileSize { + return "", fmt.Errorf("%s exceeds max file size (%d > %d)", widgetFilePath, f.UncompressedSize64, maxFileSize) + } rc, err := f.Open() if err != nil { return "", err @@ -321,6 +384,10 @@ func getWidgetIDFromMPK(mpkPath string) (string, error) { if err != nil { return "", err } + totalExtracted += uint64(len(data)) + if totalExtracted > maxTotalSize { + return "", fmt.Errorf("total extracted size exceeds limit (%d > %d)", totalExtracted, maxTotalSize) + } // Quick XML parse to just get the id attribute var widget struct { diff --git a/sdk/widgets/templates/README.md b/sdk/widgets/templates/README.md index aa657e6..e48cb45 100644 --- a/sdk/widgets/templates/README.md +++ b/sdk/widgets/templates/README.md @@ -1,6 +1,6 @@ # Widget Templates -This directory contains JSON templates for Mendix pluggable widgets. These templates are extracted from a reference Mendix project and embedded into the mxcli binary. +This directory contains JSON templates for Mendix pluggable widgets. These templates are extracted from a reference Mendix project and embedded into the mxcli binary via `go:embed`. ## Structure @@ -9,11 +9,11 @@ templates/ ├── mendix-11.6/ # Templates for Mendix 11.6.x │ ├── combobox.json # com.mendix.widget.web.combobox.Combobox │ ├── datagrid.json # com.mendix.widget.web.datagrid.Datagrid +│ ├── gallery.json # com.mendix.widget.web.gallery.Gallery │ ├── datagrid-text-filter.json # DatagridTextFilter │ ├── datagrid-date-filter.json # DatagridDateFilter │ ├── datagrid-dropdown-filter.json │ └── datagrid-number-filter.json -├── mendix-10.x/ # Templates for older versions (if needed) └── README.md ``` @@ -27,8 +27,45 @@ Each template is a JSON file containing **both** the `CustomWidgetType` and `Wid "name": "Combo box", "version": "11.6.0", "extractedFrom": "PageTemplates.Customer_NewEdit", - "type": { ... }, // The full CustomWidgetType BSON converted to JSON - "object": { ... } // The default WidgetObject with all property values + "type": { + "$ID": "aa000000000000000000000000000001", + "$Type": "CustomWidgets$CustomWidgetType", + "WidgetId": "com.mendix.widget.web.combobox.Combobox", + "PropertyTypes": [ + { + "$ID": "aa000000000000000000000000000010", + "$Type": "CustomWidgets$WidgetPropertyType", + "PropertyKey": "attributeEnumeration", + "ValueType": { + "$ID": "aa000000000000000000000000000011", + "Type": "Attribute", + "DefaultValue": "" + } + } + ] + }, + "object": { + "$ID": "aa000000000000000000000000000100", + "$Type": "CustomWidgets$WidgetObject", + "TypePointer": "aa000000000000000000000000000001", + "Properties": [ + 2, + { + "$ID": "aa000000000000000000000000000110", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "aa000000000000000000000000000010", + "Value": { + "$ID": "aa000000000000000000000000000111", + "$Type": "CustomWidgets$WidgetValue", + "AttributeRef": null, + "DataSource": null, + "PrimitiveValue": "", + "Widgets": [2], + "Selection": "None" + } + } + ] + } } ``` @@ -36,11 +73,59 @@ Each template is a JSON file containing **both** the `CustomWidgetType` and `Wid The `type` field defines the widget's PropertyTypes (schema), while the `object` field contains the actual property values with correct defaults. Studio Pro expects: -1. **Consistent IDs**: `object.Properties[].TypePointer` must reference valid `type.ObjectType.PropertyTypes[].$ID` values -2. **All properties present**: Every PropertyType must have a corresponding WidgetProperty in the object +1. **Consistent cross-references**: `object.Properties[].TypePointer` must reference valid `type.PropertyTypes[].$ID` values; `object.TypePointer` must reference `type.$ID` +2. **All properties present**: Every PropertyType in the Type must have a corresponding WidgetProperty in the Object 3. **Correct default values**: Properties like `TextTemplate` need proper `Forms$ClientTemplate` structures, not null -Without the `object` field, mxcli must build the WidgetObject from scratch, which is error-prone and often triggers "widget definition has changed" warnings in Studio Pro. +Without the `object` field, mxcli must build the WidgetObject from scratch, which is error-prone and often triggers CE0463 "widget definition has changed" in Studio Pro. + +### ID Cross-Reference Structure + +``` +Type Object +├─ $ID ◄──────────────────────────── TypePointer (WidgetObject → CustomWidgetType) +└─ PropertyTypes[] └─ Properties[] + ├─ $ID ◄──────────────────────────── TypePointer (WidgetProperty → WidgetPropertyType) + └─ ValueType └─ Value + └─ $ID ◄──────────────────────────── TypePointer (WidgetValue → ValueType) +``` + +At load time, all `$ID` values are remapped to fresh UUIDs. The same mapping is applied to both Type and Object, preserving these cross-references. + +## Runtime Loading Pipeline + +`GetTemplateFullBSON()` in `loader.go` executes a 3-phase pipeline: + +### Phase 1: Collect IDs + +`collectIDs()` recursively walks both `type` and `object` JSON, creates `oldID → newUUID` mapping for every `$ID` field. + +### Phase 2: Convert Type JSON → BSON + +`jsonToBSONWithMappingAndObjectType()` converts the Type, replacing IDs and simultaneously extracting `PropertyTypeIDMap`: + +``` +PropertyTypeIDMap["attributeEnumeration"] = { + PropertyTypeID: "newUUID-010", // remapped $ID of WidgetPropertyType + ValueTypeID: "newUUID-011", // remapped $ID of ValueType + DefaultValue: "", + ValueType: "Attribute", +} +``` + +This map is the bridge between `.def.json` property keys and the BSON structure — the engine uses it to locate which WidgetProperty to modify for each mapping. + +### Phase 3: Convert Object JSON → BSON + +`jsonToBSONObjectWithMapping()` converts the Object using the same ID mapping. `TypePointer` fields are specially handled to ensure they point to the new IDs from Phase 2. + +### Placeholder Leak Detection + +After both phases, `containsPlaceholderID()` checks for any remaining `aa000000`-prefix IDs. If found, the load fails immediately rather than producing a corrupt MPR. + +### MPK Augmentation + +Before the 3-phase pipeline, `augmentFromMPK()` checks if the project has a newer `.mpk` for the widget (in `project/widgets/`). If found, it deep-clones the template and merges property changes from the `.mpk` XML definition, adding missing properties and removing stale ones. This reduces CE0463 from widget version drift. ## Extracting New Templates @@ -50,19 +135,23 @@ When extracting templates, **always use widgets that have been created or "fixed ### Extraction Process -1. **Create the widget in Studio Pro** - Add the widget to a page in Studio Pro and configure it with default settings +1. **Create the widget in Studio Pro** — Add the widget to a page and configure it with default settings -2. **If updating an existing template** - If Studio Pro shows "widget definition has changed", right-click and select "Update widget" to let Studio Pro fix it +2. **If updating an existing template** — If Studio Pro shows "widget definition has changed", right-click and select "Update widget" to let Studio Pro fix it -3. **Extract using mxcli** (planned feature): +3. **Extract the BSON**: ```bash -mxcli extract-templates -p /path/to/project.mpr -o sdk/widgets/templates/mendix-11.6/ +# Dump the page containing the widget +mxcli bson dump -p App.mpr --type page --object "Module.TestPage" --format json + +# Extract the CustomWidget's Type and Object fields from the JSON output +# Save as templates/mendix-11.6/widgetname.json ``` -4. **Manual extraction** (current method): -```go -// Use reader.GetRawUnit() to get the page, then extract CustomWidget.Type and CustomWidget.Object -// Convert BSON binary IDs to hex strings for JSON storage +4. **Extract skeleton .def.json** (for new widgets): +```bash +mxcli widget extract --mpk widgets/MyWidget.mpk +# Generates .mxcli/widgets/mywidget.def.json with auto-inferred mappings ``` ### Verifying Templates @@ -71,10 +160,13 @@ After updating a template, verify it works: ```bash # Create a test page with the widget -mxcli -p test.mpr -c "CREATE PAGE Test.TestPage ... DATAGRID ..." +mxcli -p test.mpr -c "CREATE PAGE Test.TestPage ... COMBOBOX ..." # Check for errors (should have no CE0463 errors) -mx check test.mpr +~/.mxcli/mxbuild/*/modeler/mx check test.mpr + +# Compare BSON if issues persist +mxcli bson dump -p test.mpr --type page --object "Test.TestPage" --format ndsl ``` ## Usage @@ -82,39 +174,30 @@ mx check test.mpr Templates are automatically used when creating pluggable widgets via MDL: ```sql -COMBOBOX myCombo ATTRIBUTE Country; +COMBOBOX myCombo (Label: 'Country', Attribute: Country) ``` -### Priority Chain - -When creating a pluggable widget, mxcli uses this priority: - -1. **Embedded template** (from this directory) - Ensures consistent results across all projects -2. **Clone from project** - Falls back to extracting from an existing widget in the target project -3. **Minimal fallback** - Creates a minimal widget definition (may show warnings in Studio Pro) +### 3-Tier Widget Registry -### Why Templates Are Needed +When creating a pluggable widget, mxcli resolves definitions and templates: -Mendix pluggable widgets (like ComboBox, DataGrid2) require a full `CustomWidgetType` definition with 50+ PropertyTypes. These definitions are embedded in each widget instance in the MPR file. Without the complete definition, Mendix will show "widget definition has changed" warnings. +| Priority | Location | Scope | +|----------|----------|-------| +| 1 (highest) | `/.mxcli/widgets/*.def.json` | Project-specific overrides | +| 2 | `~/.mxcli/widgets/*.def.json` | Global user definitions | +| 3 (lowest) | `sdk/widgets/definitions/*.def.json` (embedded) | Built-in definitions | -By embedding templates extracted from a known-good project, mxcli can create widgets that are fully compatible with Mendix Studio Pro. +Each `.def.json` declares property mappings and child slots; the `PluggableWidgetEngine` applies them to the BSON template at build time. See `docs/plans/2026-03-25-pluggable-widget-engine-design.md` for the full architecture. -### Known Limitation: Widget Version Drift +### Widget Version Drift -Static templates are tied to the widget version they were extracted from. If the target project has a **newer** version of the widget `.mpk` (in `widgets/`), Studio Pro will detect that the serialized Type definition doesn't match the installed widget and report CE0463. +Static templates are tied to the widget version they were extracted from. If the target project has a **newer** `.mpk`, the MPK augmentation mechanism (described above) handles this at runtime by merging property changes from the `.mpk` XML. -For example, the ComboBox template was extracted from a Mendix 11.6.0 project, but a 11.6.3 project may ship ComboBox v2.5.0 which added 3 new properties (`staticDataSourceCaption`, `staticDataSourceCustomContent`, `staticDataSourceValue`). Our template lacks these → CE0463. - -**The correct long-term fix**: read the widget definition from the project's actual `widgets/*.mpk` file at runtime instead of relying on static templates. The `.mpk` is a ZIP containing an XML schema (e.g., `Combobox.xml`) that defines all property keys, types, and defaults. Two approaches: - -1. **Parse `.mpk` XML, generate full BSON** — map each XML property type (`attribute`, `expression`, `widgets`, `textTemplate`, etc.) to the BSON structure with correct defaults. Eliminates version drift entirely. -2. **Augment static template from `.mpk` at runtime** — keep the current template for BSON structure patterns, but read the `.mpk` XML to discover which properties should exist, adding missing ones and removing stale ones. - -Either way, the `.mpk` in the project's `widgets/` folder is the **source of truth** for what properties a widget should have. +For cases where augmentation is insufficient, extract a fresh template from a Studio Pro project using the newer widget version. ## TextTemplate Property Requirements -Properties with `"Type": "TextTemplate"` in the Type definition require special handling. They cannot be `null` in the Object section. +Properties with `"Type": "TextTemplate"` in the Type definition require special handling. They **cannot** be `null` in the Object section. ### Problem: CE0463 "widget definition has changed" @@ -170,3 +253,15 @@ Filter widgets commonly have TextTemplate properties: - **DateFilter**: `placeholder`, `screenReaderButtonCaption`, `screenReaderCalendarCaption`, `screenReaderInputCaption` - **DropdownFilter**: `emptyOptionCaption`, `ariaLabel`, `emptySelectionCaption`, `filterInputPlaceholderCaption` - **NumberFilter**: `placeholder`, `screenReaderButtonCaption`, `screenReaderInputCaption` + +## Key Source Files + +| File | Purpose | +|------|---------| +| `sdk/widgets/loader.go` | Template loading, 3-phase ID remapping, MPK augmentation, placeholder detection | +| `sdk/widgets/mpk/mpk.go` | .mpk ZIP parsing, XML property extraction, FindMPK | +| `sdk/widgets/definitions/*.def.json` | Built-in widget definition files | +| `mdl/executor/widget_engine.go` | PluggableWidgetEngine, 6 operations, Build() pipeline | +| `mdl/executor/widget_registry.go` | 3-tier WidgetRegistry, load-time validation | +| `mdl/executor/cmd_pages_builder_input.go` | `updateWidgetPropertyValue()`, TypePointer matching | +| `cmd/mxcli/cmd_widget.go` | `mxcli widget extract/list` CLI commands | diff --git a/sdk/widgets/templates/mendix-11.6/gallery.json b/sdk/widgets/templates/mendix-11.6/gallery.json index c3c48fa..339431e 100644 --- a/sdk/widgets/templates/mendix-11.6/gallery.json +++ b/sdk/widgets/templates/mendix-11.6/gallery.json @@ -1,1926 +1,27 @@ { - "extractedFrom": "17ec364c-ac1a-4790-9859-8580f0278d47", + "widgetId": "com.mendix.widget.web.gallery.Gallery", "name": "Gallery", - "object": { - "$ID": "9b0cae7d97294fbf88abf3cafc51ee54", - "$Type": "CustomWidgets$WidgetObject", - "Properties": [ - 2, - { - "$ID": "2d9fecb139364f90b5466ed31d46f4f0", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "31b0a9721089471abe1c08cb8cdb5ffe", - "Value": { - "$ID": "548122aa888a40d0bbb15e8ee6bf9bc3", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "8d356407cdcb4c489b3d1f724bab3af1", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "d6c17d7292f34211b20684b4a38725da", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "978db1dae38948368fbb8459b78deb0d", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "7b02df9d9e5c42f4a658ce6044dc12b8", - "Value": { - "$ID": "40b49664f244455f834e184b4c53da12", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "3b9069e924fb49c39df1b461751170a3", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": { - "$ID": "1e4694c2700d4a5da47f590dc70517e9", - "$Type": "CustomWidgets$CustomWidgetXPathSource", - "EntityRef": { - "$ID": "3056e3d4a12b48ac8c58e43144a8b80b", - "$Type": "DomainModels$DirectEntityRef", - "Entity": "PgTest.Customer" - }, - "ForceFullObjects": false, - "SortBar": { - "$ID": "19fd730b1ce74ebd89e6535328b94913", - "$Type": "Forms$GridSortBar", - "SortItems": [ - 2, - { - "$ID": "b05e121b081c453191d5ede425af0c61", - "$Type": "Forms$GridSortItem", - "AttributeRef": { - "$ID": "3a7b8c4b0eb14c5c8ae7980083eb86f6", - "$Type": "DomainModels$AttributeRef", - "Attribute": "PgTest.Customer.Name", - "EntityRef": null - }, - "SortOrder": "Ascending" - } - ] - }, - "SourceVariable": null, - "XPathConstraint": "" - }, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "bb30b0cf807d447c8b412fe11f812062", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "6e2f1a3b890e4cbea66637392a14ac90", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "58f38ab267564da9b78fe179190a392a", - "Value": { - "$ID": "51cd4de0142146988760ac5be10c77ff", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "d426d6ce03db45129c257cbe3a35e8ac", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "Single", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "492aa01efc0f4e7e987134391a6c2a2a", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "10c3c8dc450b48b8af4a2b23708893ec", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "6509b8c61dbd4485897d8b0929aa3d15", - "Value": { - "$ID": "d53b7968c2fa4c8eb5b7370f3399f6cf", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "3e0452d1554342c1ae756ac4d6428ebe", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "clear", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "405c5594324241198c7d8ec673ea2626", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "11d02208132b4dad963f932913813310", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "f929496fda154e9ea5a0bee0c8055fe9", - "Value": { - "$ID": "68aa9f594e2a48b9bc9f32a787e9575f", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "d4bb8ac9a1214c83b8226b09b82d7639", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "false", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "cfc0f50efae9455cbbd1f6d06c97cb19", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "c789d8f25d8040a2a7a3de5fcc2d0256", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "6c97ba7e6cd44dcfa7d7ec4fe98d5918", - "Value": { - "$ID": "6285e926f1124d19afe65770938f745a", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "aca8773ba0634335b9dd8b4db198da95", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "5cadb97bf49546a5afaee593b6d2f398", - "Widgets": [ - 2, - { - "$ID": "6b55ceb70c324de0b6fb45b4c8b607e6", - "$Type": "Forms$DynamicText", - "Appearance": { - "$ID": "29b23d288c944097a8c4cfa52f1a835f", - "$Type": "Forms$Appearance", - "Class": "", - "DesignProperties": [ - 3 - ], - "DynamicClasses": "", - "Style": "" - }, - "ConditionalVisibilitySettings": null, - "Content": { - "$ID": "a5347beef24a402d9c18485d1382b0f8", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "6550515be4a74d1c8925eaa9402dad13", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "bdcc89f4c6a149fbb3692a831ee79169", - "$Type": "Texts$Text", - "Items": [ - 3, - { - "$ID": "38704f9f3d244012a04ef123530ec4c6", - "$Type": "Texts$Translation", - "LanguageCode": "en_US", - "Text": "{Name}" - } - ] - } - }, - "Name": "custName", - "NativeAccessibilitySettings": null, - "NativeTextStyle": "Text", - "RenderMode": "Text", - "TabIndex": 0 - }, - { - "$ID": "e77bb30cf56b4e2c84e5701eda0dd6b9", - "$Type": "Forms$DynamicText", - "Appearance": { - "$ID": "6483460c2b0f4162b0f19fcbb95843a8", - "$Type": "Forms$Appearance", - "Class": "", - "DesignProperties": [ - 3 - ], - "DynamicClasses": "", - "Style": "" - }, - "ConditionalVisibilitySettings": null, - "Content": { - "$ID": "bfb041bf438f4f30b00169d597bdced9", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "0d65fe67ebdf45e2b12b2986844b32d2", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "e233d87f63d7499f86010ebbacc33484", - "$Type": "Texts$Text", - "Items": [ - 3, - { - "$ID": "f2f9b086e2c3437081bc8f0fab7a15c3", - "$Type": "Texts$Translation", - "LanguageCode": "en_US", - "Text": "{Email}" - } - ] - } - }, - "Name": "custEmail", - "NativeAccessibilitySettings": null, - "NativeTextStyle": "Text", - "RenderMode": "Text", - "TabIndex": 0 - }, - { - "$ID": "ea0d34709e764cbf8bfc0fef444d0a29", - "$Type": "Forms$DynamicText", - "Appearance": { - "$ID": "87b73ea2217a449fb3140280329142cc", - "$Type": "Forms$Appearance", - "Class": "", - "DesignProperties": [ - 3 - ], - "DynamicClasses": "", - "Style": "" - }, - "ConditionalVisibilitySettings": null, - "Content": { - "$ID": "1c7c58e85d4c4a0080a6fa119815e43a", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "c69c01faf4274af38df74a319233089a", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "7b0c1431871045088a12244653901dfc", - "$Type": "Texts$Text", - "Items": [ - 3, - { - "$ID": "a6ff0b28f0fe439ba2c4be400fd7efac", - "$Type": "Texts$Translation", - "LanguageCode": "en_US", - "Text": "{City}" - } - ] - } - }, - "Name": "custCity", - "NativeAccessibilitySettings": null, - "NativeTextStyle": "Text", - "RenderMode": "Text", - "TabIndex": 0 - } - ], - "XPathConstraint": "" - } - }, - { - "$ID": "bc1a40353e774ce3bf8362e0f5d4c664", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "d5311097fe25421ea154aa10b878a98f", - "Value": { - "$ID": "12f2d94b94e446929827ade58d9af522", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "e107f8a55e1a4f27a7fef7df4455ad21", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "false", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "31d9a35c3e974c0ba74882887b4d0f45", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "10c439ba66de4fcabcbfd73785692969", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "8f340a34ad2a4324a78d68aeb7c9c882", - "Value": { - "$ID": "779c2aa22e1e4950b3cd3ebfc8da8fa0", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "91432b563b3943b28876e8dd2b64e9a7", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "1", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "cf9eb5e4f3b14cfe8c0e37028c942ac7", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "45d5241ffd234f2f8a2379d7af8d772f", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "04f980cab72c4affb41e02d7e4af34d7", - "Value": { - "$ID": "b9fd1db89e8a4e39844a389301f65e47", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "79c6ab0b8dfb40f6842d5c593c3aa7d8", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "1", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "660d46ce13174f57b1c66ca48734b0ed", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "bb544312749a4abdab30d8ff36e3fa6f", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "0b0a9ffd0f1a4ed5a241f25bcf2bdbda", - "Value": { - "$ID": "65144c048fdc4100addba7388cb5544d", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "781dc2eaad804b64aceba76669cfe7d9", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "1", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "3ddf7e99ae424a058abb511b1a5ca407", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "84a2432e08df4502b8b961ad5c91627a", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "f2473f5cde00484ea9e49c1aef65ad94", - "Value": { - "$ID": "244dcbbbb2be403a99126dcd3b646508", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "32e06175895849a991c9881f8e6f61b4", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "20", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "14208c2884f74d6e80f38452433fe656", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "c5988dd60a7e43488640d6336c9d2241", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "fc6e9c443b194c2aafa7ea730bc8981b", - "Value": { - "$ID": "b0d89de539f34c1ca1e6b9fca6399dde", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "c48a9d6b632f4cd1bbd0f43d53542353", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "buttons", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "1982af8c020d4130b51381a095cf70bc", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "9f0116408ddd4c8eb0b86ca018de6b9f", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "9d8ba4410ec741428ad3b687d85ff01f", - "Value": { - "$ID": "6e6f5f71b8f84eaab4d6a4f6b78aa3dc", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "845318144de143c38e1d052c65eb7967", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "false", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "99bd880b563146ddbd134b24e349184b", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "0742f3faf1114ace990a227a43d7e0a7", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "a375ce9727114e87a9d6f9f99f2ab273", - "Value": { - "$ID": "b58bb99d9e1145b686bbfc2808056464", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "82a1bb11d698446aab6e2dd4aa3f7d4b", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "always", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "88f8002d05e447c1a05f9cb26d62452f", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "0f4d347203864f1c9386bc7c9b4fbd06", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "4d33e089d6674b03825cd502cc3cfbc0", - "Value": { - "$ID": "93a043e1a1bf496d869be864180549e4", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "0ca5a5004b0b40efbc244d0ad2d02dae", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "bottom", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "f0d7674deea345deba27b38afb8623b6", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "4b8878b2e4514c52958ca6d0d2ef18b0", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "32a8013811444fc1bbab8658a9320748", - "Value": { - "$ID": "3be01495d7f3452486b618d333d84744", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "e1df5cb8883141ed84e2e814f54c258c", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "a41df587f3ee4577984a619e2492155b", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "6a7936ac75a9442a8ec9c166789c5186", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "b53bbbd25d9a453f9dc8e243d6cd91c8", - "Value": { - "$ID": "19c67435985c485e907f716eb4ebcbf2", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "c7887ff3e7574193bf2145bd7628c1d7", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "none", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "bdc82f03aef74c3dbba1bbc3466661ef", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "f227deaf9b9548358495260d9cf657b2", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "ee55b425bb8042dc9d2c8212b7b4cd70", - "Value": { - "$ID": "c11646ac52774b3ba3879038827c73f9", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "f59b4ec6ef2f4d459f61517ed98a928b", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "e387c1970f184dd6881a3f56cbebe141", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "ad65637df42d4931a4cea5ede38e3e6a", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "ecf8d48a9990406cbc6d46c96972eeb2", - "Value": { - "$ID": "88ccd48657024842a573bce2743f0ac3", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "d9020b984fad41acbc8396113444c613", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "37e4f35bbd144641a2c85547d55ec82d", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "3ded206ffec447bfa791504b6d2bb1a9", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "7b4c9566e96b4b028786d3eb9e481c37", - "Value": { - "$ID": "3eb7cabfb2d948e292dae679389466fe", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "66bfa6155a254729a3d275ab56d21959", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "single", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "0dd3285dccee4455b7d4529c8205b09d", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "ea914d85907444ff92fdf0fa8939f8df", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "41affea39a70496eacc65f9020a7ca51", - "Value": { - "$ID": "aa3384db5a634ec1a6cc9f3314f1f567", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "a76c1ae13ddb4900ae364fc9aec3c31a", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "8bca76ec55ef446bb8920f78b45fb81f", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "74fdc1e204fc4769b7f399990669c333", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "8900de019370404c8ad1c2d7d2c91083", - "Value": { - "$ID": "546ff5573afd4e61b78a6ec5f2489c7d", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "4f92e33d9f094aa7b90cf4b1edbd1167", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "99e8ceb69ae7404093f14ee2178d30d8", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "d7b02ced43a1433fbbc6859f6e0f22e6", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "012cb6be356041ed8b4832e7b5285435", - "Value": { - "$ID": "237c6a41be29491db5ac756d31621420", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "7475478b5c054136b33185a28dbe9aa8", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "attribute", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "dbcc5f5ce880497ea86cd6124f790b6c", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "2ab44ab98b144e8091b01abfaaff6221", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "a9bba43d45524fdebc5646cd21825fa5", - "Value": { - "$ID": "48a24e5942bd4bb8bc58601eb6c34ebc", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "42697b766ef5484396294ae151132d7e", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "76e2cdf903714d4091be46587a92243d", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "382de7a6445e4fac80cff3611eb3ce68", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "f2053bdf2a794c6f8e2f13870bb39481", - "Value": { - "$ID": "602854fc3f4046e9b30298ad1e210f8c", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "65324de5a46046048c4dbb86ce0edfd2", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "c85d44b4223746c58da7acdcdde7c1c5", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "13b1e09f6e924cc4b68c0b2a333ba3e6", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "079e690b9a5540199ed9457cfda85700", - "Value": { - "$ID": "becbad6ecee4417b958493b5c79f81fc", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "b5ba892d6f724f1d84ddc7df3569569d", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "true", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "7e14afd20e9a434da505f43544390480", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "0dcb2802c3b647d1a24de2b75d0be2c9", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "45d0d05e8bfa44fb9f9ca1b15601f4f2", - "Value": { - "$ID": "363e08dbded84cec8593fe9eb415ff82", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "61db5b151cf04d27bee849521656bc24", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "true", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": null, - "TranslatableValue": null, - "TypePointer": "a6ce288bfa6040fb882d7cfaf3fbe247", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "3467f08458064a128757a1c7108ddf0b", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "83f940d2d8e241ca814f598021dba1a9", - "Value": { - "$ID": "acccdc025acb4c18abc36f57f3829897", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "63de929afcaf4ffdb2c20eeb19a0b907", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "f02241b169a844d387a81ab9c63e08d0", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "e3fb76efa0c1450c9c41b32315631276", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "2007a5ee1d1243f68544a315f5f2c3d4", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "e6046a4f671442d5a6f624d4b13fec78", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "8d576228a98a4f329c98fa31a0a1815a", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "a94caf85279a41039ac8704c56ff144c", - "Value": { - "$ID": "abba43c7e0654338be83eec49be091df", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "6520509ae5614223a0b8feae57208154", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "9e1bb45e44f2461685181ec042625610", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "db31be2308d54bfebc59a29b2b3ed274", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "71d79523dc5f4fe1b41b854a3d48191e", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "86524910ce114f51974d6a0282d2b154", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "3222b43ef9e94d46a22af78a7f095471", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "f7f250cf1d13489a81e481cea8cb883b", - "Value": { - "$ID": "f9e9a352b96c4750953d562289b40162", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "2d68dd6a251947a4900607a196d71546", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "caa8e784dab04e1db9f2e2854aee0e65", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "8d2ef08251a9436794e176433c48e493", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "4a1769e7e7214a3b939c81d985e2c500", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "617786fc63b7407cbfdb58763c59f035", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "36ca0fbf5eb349ceaf57c2af8b3a3e88", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "27d0cddf48cb423188d30c919e30e4fc", - "Value": { - "$ID": "34011eb4563f490282bc94c2dcb15345", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "cce6fc9166d148adb8c9043a17aec702", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "1c9ade5b375b44328b8b3ef8d096e60e", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "5ab0e827ef0349b4b2394f3c17fc8f83", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "d07cc2829cad463e806b704b1450280e", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "1e8cf5197fab423f9b8b32ff4e3f8cd5", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "00959aeb8d2d4e81a9021db667b2d96c", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "b86b18c5971947d78d28751c71e3859f", - "Value": { - "$ID": "eb1e8074a3d24e6dbda0c60709805724", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "0e9f2f9752094239bc2ed15f8f232b40", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "c1f84ec0ff014de58c55566b529f2a6d", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "b3d72233516648c09218c60d0e654018", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "7c820bfa023a4a27880254fcde0a8cbb", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "66908f0edb2b4b6c9d768de04f866d8e", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - }, - { - "$ID": "738181a29a084e5199e60a4908c0a2d6", - "$Type": "CustomWidgets$WidgetProperty", - "TypePointer": "bbb8f2f283f3498d817401dafd1f7d0d", - "Value": { - "$ID": "66d7874bdd1f4cb6af4124eadc008c83", - "$Type": "CustomWidgets$WidgetValue", - "Action": { - "$ID": "8475deca8c7548c39a1c426bd2b943f8", - "$Type": "Forms$NoAction", - "DisabledDuringExecution": true - }, - "AttributeRef": null, - "DataSource": null, - "EntityRef": null, - "Expression": "", - "Form": "", - "Icon": null, - "Image": "", - "Microflow": "", - "Nanoflow": "", - "Objects": [ - 2 - ], - "PrimitiveValue": "", - "Selection": "None", - "SourceVariable": null, - "TextTemplate": { - "$ID": "00d3b9ec0bea4908a88b2f1b1eca71fc", - "$Type": "Forms$ClientTemplate", - "Fallback": { - "$ID": "bcbf270b67dc4a3483517a448f7c4ee7", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - }, - "Parameters": [ - 2 - ], - "Template": { - "$ID": "e4fd420d59d84cc49b18aa9c721dd8cb", - "$Type": "Texts$Text", - "Items": [ - 3 - ] - } - }, - "TranslatableValue": null, - "TypePointer": "71f1cbf825c74604a2b978fefd922903", - "Widgets": [ - 2 - ], - "XPathConstraint": "" - } - } - ], - "TypePointer": "d1986b3305b14cb1830e64290f4e4495" - }, - "type": { - "$ID": "b42caa640fdc41709081ff852d6aea61", - "$Type": "CustomWidgets$CustomWidgetType", - "HelpUrl": "https://docs.mendix.com/appstore/modules/gallery", - "ObjectType": { - "$ID": "d1986b3305b14cb1830e64290f4e4495", - "$Type": "CustomWidgets$WidgetObjectType", - "PropertyTypes": [ - 2, - { - "$ID": "31b0a9721089471abe1c08cb8cdb5ffe", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Filters placeholder", - "Category": "General::General", - "Description": "", - "IsDefault": false, - "PropertyKey": "filtersPlaceholder", - "ValueType": { - "$ID": "d6c17d7292f34211b20684b4a38725da", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": false, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Widgets" - } - }, - { - "$ID": "7b02df9d9e5c42f4a658ce6044dc12b8", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Data source", - "Category": "General::General", - "Description": "", - "IsDefault": false, - "PropertyKey": "datasource", - "ValueType": { - "$ID": "bb30b0cf807d447c8b412fe11f812062", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": true, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "DataSource" - } - }, - { - "$ID": "58f38ab267564da9b78fe179190a392a", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Selection", - "Category": "General::General", - "Description": "", - "IsDefault": false, - "PropertyKey": "itemSelection", - "ValueType": { - "$ID": "492aa01efc0f4e7e987134391a6c2a2a", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "datasource", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1, - "None", - "Single", - "Multi" - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Selection" - } - }, - { - "$ID": "6509b8c61dbd4485897d8b0929aa3d15", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Item click toggles selection", - "Category": "General::General", - "Description": "Defines item selection behavior.", - "IsDefault": false, - "PropertyKey": "itemSelectionMode", - "ValueType": { - "$ID": "405c5594324241198c7d8ec673ea2626", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "clear", - "EntityProperty": "", - "EnumerationValues": [ - 2, - { - "$ID": "e992a4d2c2934ec5bfcad4503a5ed4d8", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Yes", - "_Key": "toggle" - }, - { - "$ID": "2b6ce9fc257949b981f34b6bafdcd348", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "No", - "_Key": "clear" - } - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Enumeration" - } - }, - { - "$ID": "f929496fda154e9ea5a0bee0c8055fe9", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Keep selection", - "Category": "General::General", - "Description": "If enabled, selected items will stay selected unless cleared by the user or a Nanoflow.", - "IsDefault": false, - "PropertyKey": "keepSelection", - "ValueType": { - "$ID": "cfc0f50efae9455cbbd1f6d06c97cb19", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "false", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Boolean" - } - }, - { - "$ID": "6c97ba7e6cd44dcfa7d7ec4fe98d5918", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Content placeholder", - "Category": "General::General", - "Description": "", - "IsDefault": false, - "PropertyKey": "content", - "ValueType": { - "$ID": "5cadb97bf49546a5afaee593b6d2f398", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "datasource", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": false, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Widgets" - } - }, - { - "$ID": "d5311097fe25421ea154aa10b878a98f", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Show refresh indicator", - "Category": "General::General", - "Description": "Show a refresh indicator when the data is being loaded.", - "IsDefault": false, - "PropertyKey": "refreshIndicator", - "ValueType": { - "$ID": "31d9a35c3e974c0ba74882887b4d0f45", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "false", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Boolean" - } - }, - { - "$ID": "8f340a34ad2a4324a78d68aeb7c9c882", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Desktop columns", - "Category": "General::Columns", - "Description": "", - "IsDefault": false, - "PropertyKey": "desktopItems", - "ValueType": { - "$ID": "cf9eb5e4f3b14cfe8c0e37028c942ac7", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "1", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": true, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Integer" - } - }, + "version": "11.6.4", + "extractedFrom": "e52a9db9-1a68-4e67-8487-494fb31efb88", + "type": { + "$ID": "b86c4ff9e47d2b4bb35227d58d8ef7ed", + "$Type": "CustomWidgets$CustomWidgetType", + "HelpUrl": "https://docs.mendix.com/appstore/modules/gallery", + "ObjectType": { + "$ID": "25db8bdc0d4afb4382846d3d22271efd", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, { - "$ID": "04f980cab72c4affb41e02d7e4af34d7", + "$ID": "f4297805fc0a2844b8d8e770ab0ab185", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Tablet columns", - "Category": "General::Columns", + "Caption": "Enable advanced options", + "Category": "General::General", "Description": "", "IsDefault": false, - "PropertyKey": "tabletItems", + "PropertyKey": "advanced", "ValueType": { - "$ID": "660d46ce13174f57b1c66ca48734b0ed", + "$ID": "9dd44ee2b43dd349bab35621e78c4211", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -1934,7 +35,7 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "1", + "DefaultValue": "false", "EntityProperty": "", "EnumerationValues": [ 2 @@ -1958,19 +59,19 @@ "Translations": [ 2 ], - "Type": "Integer" + "Type": "Boolean" } }, { - "$ID": "0b0a9ffd0f1a4ed5a241f25bcf2bdbda", + "$ID": "62b9b1d85ee7974fac2a80c72fbe646f", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Phone columns", - "Category": "General::Columns", + "Caption": "Data source", + "Category": "General::General", "Description": "", "IsDefault": false, - "PropertyKey": "phoneItems", + "PropertyKey": "datasource", "ValueType": { - "$ID": "3ddf7e99ae424a058abb511b1a5ca407", + "$ID": "41eee3044915484abe6fa417a2821162", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -1984,13 +85,13 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "1", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ 2 ], "IsLinked": false, - "IsList": false, + "IsList": true, "IsMetaData": false, "IsPath": "No", "Multiline": false, @@ -2008,19 +109,19 @@ "Translations": [ 2 ], - "Type": "Integer" + "Type": "DataSource" } }, { - "$ID": "f2473f5cde00484ea9e49c1aef65ad94", + "$ID": "3571441c44e92d46824f5b7faf9c6369", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Page size", - "Category": "General::Pagination", + "Caption": "Selection", + "Category": "General::General", "Description": "", "IsDefault": false, - "PropertyKey": "pageSize", + "PropertyKey": "itemSelection", "ValueType": { - "$ID": "14208c2884f74d6e80f38452433fe656", + "$ID": "37a3e452d97bf547ab6f09351e50b683", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2032,9 +133,9 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "", + "DataSourceProperty": "datasource", "DefaultType": "None", - "DefaultValue": "20", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ 2 @@ -2052,25 +153,28 @@ "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ - 1 + 1, + "None", + "Single", + "Multi" ], "SetLabel": false, "Translations": [ 2 ], - "Type": "Integer" + "Type": "Selection" } }, { - "$ID": "fc6e9c443b194c2aafa7ea730bc8981b", + "$ID": "0b4a1ed0814e814ab90f9bbc420680d3", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Pagination", - "Category": "General::Pagination", - "Description": "", + "Caption": "Item click toggles selection", + "Category": "General::General", + "Description": "Defines item selection behavior.", "IsDefault": false, - "PropertyKey": "pagination", + "PropertyKey": "itemSelectionMode", "ValueType": { - "$ID": "1982af8c020d4130b51381a095cf70bc", + "$ID": "e567dba05dcd134c8c8a40f975d428d5", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2084,27 +188,21 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "buttons", + "DefaultValue": "clear", "EntityProperty": "", "EnumerationValues": [ 2, { - "$ID": "458dedc0d3884eccb7800c4dc9701c92", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Paging buttons", - "_Key": "buttons" - }, - { - "$ID": "08337e9751b040b480bc1e587d393ac7", + "$ID": "5eb97f0addb04947a296804dd60ddb3b", "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Virtual scrolling", - "_Key": "virtualScrolling" + "Caption": "Yes", + "_Key": "toggle" }, { - "$ID": "34fbe118e55248e89c5d1a5e9077d83c", + "$ID": "ab258c67f38b8c4fa6619c675324ee35", "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Load more", - "_Key": "loadMore" + "Caption": "No", + "_Key": "clear" } ], "IsLinked": false, @@ -2130,15 +228,15 @@ } }, { - "$ID": "9d8ba4410ec741428ad3b687d85ff01f", + "$ID": "1c5af78035daad4c9771ab01956ae5fa", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Show total count", - "Category": "General::Pagination", + "Caption": "Content placeholder", + "Category": "General::General", "Description": "", "IsDefault": false, - "PropertyKey": "showTotalCount", + "PropertyKey": "content", "ValueType": { - "$ID": "99bd880b563146ddbd134b24e349184b", + "$ID": "b3862d42ae20bc4e8be9fde4097a4e29", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2150,9 +248,9 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "", + "DataSourceProperty": "datasource", "DefaultType": "None", - "DefaultValue": "false", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ 2 @@ -2166,7 +264,7 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": true, + "Required": false, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2176,19 +274,19 @@ "Translations": [ 2 ], - "Type": "Boolean" + "Type": "Widgets" } }, { - "$ID": "a375ce9727114e87a9d6f9f99f2ab273", + "$ID": "f2dcfd0bba10874c95f47d4c45f4c526", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Show paging buttons", - "Category": "General::Pagination", + "Caption": "Desktop columns", + "Category": "General::Columns", "Description": "", "IsDefault": false, - "PropertyKey": "showPagingButtons", + "PropertyKey": "desktopItems", "ValueType": { - "$ID": "88f8002d05e447c1a05f9cb26d62452f", + "$ID": "8f68eb2f6aaed844a1e99f69043657c5", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2202,22 +300,10 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "always", + "DefaultValue": "1", "EntityProperty": "", "EnumerationValues": [ - 2, - { - "$ID": "661dd3106bdf4a78bc4fadba4cac7453", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Always", - "_Key": "always" - }, - { - "$ID": "baf7389746b1434e8ab5e6dd5b28afb2", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Auto", - "_Key": "auto" - } + 2 ], "IsLinked": false, "IsList": false, @@ -2238,19 +324,19 @@ "Translations": [ 2 ], - "Type": "Enumeration" + "Type": "Integer" } }, { - "$ID": "4d33e089d6674b03825cd502cc3cfbc0", + "$ID": "358c6e49c4e54241abdd99f05965c006", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Position of pagination", - "Category": "General::Pagination", + "Caption": "Tablet columns", + "Category": "General::Columns", "Description": "", "IsDefault": false, - "PropertyKey": "pagingPosition", + "PropertyKey": "tabletItems", "ValueType": { - "$ID": "f0d7674deea345deba27b38afb8623b6", + "$ID": "c9a6cd6b5f1a3c4babef4b3e9baa3dab", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2264,28 +350,10 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "bottom", + "DefaultValue": "1", "EntityProperty": "", "EnumerationValues": [ - 2, - { - "$ID": "ad7c9f810bbf46f5bb06eeb11a0fc298", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Below grid", - "_Key": "bottom" - }, - { - "$ID": "958b30f7a7654da1b8e960c9476db678", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Above grid", - "_Key": "top" - }, - { - "$ID": "af405e28f14641af8647b9763d7ad07a", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Both", - "_Key": "both" - } + 2 ], "IsLinked": false, "IsList": false, @@ -2306,19 +374,19 @@ "Translations": [ 2 ], - "Type": "Enumeration" + "Type": "Integer" } }, { - "$ID": "32a8013811444fc1bbab8658a9320748", + "$ID": "c38078fd298f9a40bd7dd8437d939f94", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Load more caption", - "Category": "General::Pagination", + "Caption": "Phone columns", + "Category": "General::Columns", "Description": "", "IsDefault": false, - "PropertyKey": "loadMoreButtonCaption", + "PropertyKey": "phoneItems", "ValueType": { - "$ID": "a41df587f3ee4577984a619e2492155b", + "$ID": "f821313a6c7a1b4bb0f54c1fee5bda9d", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2332,7 +400,7 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "", + "DefaultValue": "1", "EntityProperty": "", "EnumerationValues": [ 2 @@ -2346,7 +414,7 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": false, + "Required": true, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2354,27 +422,21 @@ ], "SetLabel": false, "Translations": [ - 2, - { - "$ID": "e45e3f2f2bed456eb25d371703ede949", - "$Type": "CustomWidgets$WidgetTranslation", - "LanguageCode": "en_US", - "Text": "Load More" - } + 2 ], - "Type": "TextTemplate" + "Type": "Integer" } }, { - "$ID": "b53bbbd25d9a453f9dc8e243d6cd91c8", + "$ID": "3f25c5443c8fa34ebe04588fa73341a9", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Empty message", + "Caption": "Page size", "Category": "General::Items", "Description": "", "IsDefault": false, - "PropertyKey": "showEmptyPlaceholder", + "PropertyKey": "pageSize", "ValueType": { - "$ID": "bdc82f03aef74c3dbba1bbc3466661ef", + "$ID": "c532e4ea44ae0f4fba93ec27e700f296", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2388,22 +450,10 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "none", + "DefaultValue": "20", "EntityProperty": "", "EnumerationValues": [ - 2, - { - "$ID": "9291781d01a941ea8aee79a2551026b3", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "None", - "_Key": "none" - }, - { - "$ID": "64517ae1b50245edb187f6ab773bbff0", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Custom", - "_Key": "custom" - } + 2 ], "IsLinked": false, "IsList": false, @@ -2424,19 +474,19 @@ "Translations": [ 2 ], - "Type": "Enumeration" + "Type": "Integer" } }, { - "$ID": "ee55b425bb8042dc9d2c8212b7b4cd70", + "$ID": "45bd939d9451034783b7967e55675118", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Empty placeholder", + "Caption": "Pagination", "Category": "General::Items", "Description": "", "IsDefault": false, - "PropertyKey": "emptyPlaceholder", + "PropertyKey": "pagination", "ValueType": { - "$ID": "e387c1970f184dd6881a3f56cbebe141", + "$ID": "333d9ad699cc67459aeca95b8e9e94f0", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2450,10 +500,22 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "", + "DefaultValue": "buttons", "EntityProperty": "", "EnumerationValues": [ - 2 + 2, + { + "$ID": "109fc539b330e74483450966b9024207", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Paging buttons", + "_Key": "buttons" + }, + { + "$ID": "7cf3b31d74df494586e6b5f59d6c714b", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Virtual scrolling", + "_Key": "virtualScrolling" + } ], "IsLinked": false, "IsList": false, @@ -2464,7 +526,7 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": false, + "Required": true, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2474,76 +536,19 @@ "Translations": [ 2 ], - "Type": "Widgets" + "Type": "Enumeration" } }, { - "$ID": "ecf8d48a9990406cbc6d46c96972eeb2", + "$ID": "252d2c2006b6fc44a3256a32e457324b", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Dynamic item class", + "Caption": "Position of paging buttons", "Category": "General::Items", "Description": "", "IsDefault": false, - "PropertyKey": "itemClass", - "ValueType": { - "$ID": "37e4f35bbd144641a2c85547d55ec82d", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ - 2 - ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "datasource", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ - 2 - ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": false, - "ReturnType": { - "$ID": "df6b51260a8e4cdf9698544838b9a3f3", - "$Type": "CustomWidgets$WidgetReturnType", - "AssignableTo": "", - "EntityProperty": "", - "IsList": false, - "Type": "String" - }, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ - 2 - ], - "Type": "Expression" - } - }, - { - "$ID": "7b4c9566e96b4b028786d3eb9e481c37", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "On click trigger", - "Category": "General::Events", - "Description": "", - "IsDefault": false, - "PropertyKey": "onClickTrigger", + "PropertyKey": "pagingPosition", "ValueType": { - "$ID": "0dd3285dccee4455b7d4529c8205b09d", + "$ID": "8fdab804f5be5c43960d3c1624d08888", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2557,21 +562,21 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "single", + "DefaultValue": "below", "EntityProperty": "", "EnumerationValues": [ 2, { - "$ID": "7331a4a7fed04c2a93bb1ffbabb95ce5", + "$ID": "194368ec742a8c4190c77f157fe0472c", "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Single click", - "_Key": "single" + "Caption": "Below grid", + "_Key": "below" }, { - "$ID": "116a4c55794b4bfc971543762112ae6b", + "$ID": "c8e3a1a5aeb6e649b5a8ea1245885b5a", "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Double click", - "_Key": "double" + "Caption": "Above grid", + "_Key": "above" } ], "IsLinked": false, @@ -2597,15 +602,15 @@ } }, { - "$ID": "41affea39a70496eacc65f9020a7ca51", + "$ID": "ab11ac2ce566344488685887ae8c63bb", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "On click action", - "Category": "General::Events", + "Caption": "Empty message", + "Category": "General::Items", "Description": "", "IsDefault": false, - "PropertyKey": "onClick", + "PropertyKey": "showEmptyPlaceholder", "ValueType": { - "$ID": "8bca76ec55ef446bb8920f78b45fb81f", + "$ID": "49e96d756d860d4eab9ed3d67e216504", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2617,12 +622,24 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "datasource", + "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "", + "DefaultValue": "none", "EntityProperty": "", "EnumerationValues": [ - 2 + 2, + { + "$ID": "4235300f9771904b8d374c5577b8674c", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "None", + "_Key": "none" + }, + { + "$ID": "57a5fe151a6c9349b1d9719182204a0d", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Custom", + "_Key": "custom" + } ], "IsLinked": false, "IsList": false, @@ -2633,7 +650,7 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": false, + "Required": true, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2643,19 +660,19 @@ "Translations": [ 2 ], - "Type": "Action" + "Type": "Enumeration" } }, { - "$ID": "8900de019370404c8ad1c2d7d2c91083", + "$ID": "367900ea5d5f674194f016300e4b96a4", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "On selection change", - "Category": "General::Events", + "Caption": "Empty placeholder", + "Category": "General::Items", "Description": "", "IsDefault": false, - "PropertyKey": "onSelectionChange", + "PropertyKey": "emptyPlaceholder", "ValueType": { - "$ID": "99e8ceb69ae7404093f14ee2178d30d8", + "$ID": "9e0cb79afeb06b4a85d3c19752c63f0d", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2693,19 +710,19 @@ "Translations": [ 2 ], - "Type": "Action" + "Type": "Widgets" } }, { - "$ID": "012cb6be356041ed8b4832e7b5285435", + "$ID": "446fb3a0fef0d5478e74926f27658b54", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Store configuration in", - "Category": "Personalization::Configuration", - "Description": "When Browser local storage is selected, the configuration is scoped to a browser profile. This configuration is not tied to a Mendix user.", + "Caption": "Dynamic item class", + "Category": "General::Items", + "Description": "", "IsDefault": false, - "PropertyKey": "stateStorageType", + "PropertyKey": "itemClass", "ValueType": { - "$ID": "dbcc5f5ce880497ea86cd6124f790b6c", + "$ID": "15f31411de0b784198d947dd87405377", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2717,24 +734,12 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "", + "DataSourceProperty": "datasource", "DefaultType": "None", - "DefaultValue": "attribute", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ - 2, - { - "$ID": "550fc913c0ab43ccb327beb7ea8443da", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Attribute", - "_Key": "attribute" - }, - { - "$ID": "f270cd1c58284024a3b0981e665c61f0", - "$Type": "CustomWidgets$WidgetEnumerationValue", - "Caption": "Browser local storage", - "_Key": "localStorage" - } + 2 ], "IsLinked": false, "IsList": false, @@ -2745,8 +750,15 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": true, - "ReturnType": null, + "Required": false, + "ReturnType": { + "$ID": "dc3c1e7f68b58f4487f17fe8eee737b6", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "String" + }, "SelectableObjectsProperty": "", "SelectionTypes": [ 1 @@ -2755,37 +767,48 @@ "Translations": [ 2 ], - "Type": "Enumeration" + "Type": "Expression" } }, { - "$ID": "a9bba43d45524fdebc5646cd21825fa5", + "$ID": "a1118376074b0a4c940b1c24102d1c96", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Attribute", - "Category": "Personalization::Configuration", - "Description": "Attribute containing the personalized configuration of the capabilities. This configuration is automatically stored and loaded. The attribute requires Unlimited String.", + "Caption": "On click trigger", + "Category": "General::Events", + "Description": "", "IsDefault": false, - "PropertyKey": "stateStorageAttr", + "PropertyKey": "onClickTrigger", "ValueType": { - "$ID": "76e2cdf903714d4091be46587a92243d", + "$ID": "b1b92672f016e344bc9ba41b78e70eb7", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 ], "AllowNonPersistableEntities": false, "AllowedTypes": [ - 1, - "String" + 1 ], "AssociationTypes": [ 1 ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "", + "DefaultValue": "single", "EntityProperty": "", "EnumerationValues": [ - 2 + 2, + { + "$ID": "5c90328383abd144abc171d4fe821c71", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Single click", + "_Key": "single" + }, + { + "$ID": "76f4f5713afa2443b943639780828bfe", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Double click", + "_Key": "double" + } ], "IsLinked": false, "IsList": false, @@ -2793,10 +816,10 @@ "IsPath": "No", "Multiline": false, "ObjectType": null, - "OnChangeProperty": "onConfigurationChange", + "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": false, + "Required": true, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2806,19 +829,19 @@ "Translations": [ 2 ], - "Type": "Attribute" + "Type": "Enumeration" } }, { - "$ID": "f2053bdf2a794c6f8e2f13870bb39481", + "$ID": "423f38a21ffc6141b3d9672257df1d93", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "On change", - "Category": "Personalization::Configuration", + "Caption": "On click action", + "Category": "General::Events", "Description": "", "IsDefault": false, - "PropertyKey": "onConfigurationChange", + "PropertyKey": "onClick", "ValueType": { - "$ID": "c85d44b4223746c58da7acdcdde7c1c5", + "$ID": "7e89bd0cbbb0cc4f863038fd7c4d23cc", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2830,7 +853,7 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "", + "DataSourceProperty": "datasource", "DefaultType": "None", "DefaultValue": "", "EntityProperty": "", @@ -2860,15 +883,15 @@ } }, { - "$ID": "079e690b9a5540199ed9457cfda85700", + "$ID": "daa185638007794fb7c3183593effadc", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Store filters", - "Category": "Personalization::Configuration", + "Caption": "On selection change", + "Category": "General::Events", "Description": "", "IsDefault": false, - "PropertyKey": "storeFilters", + "PropertyKey": "onSelectionChange", "ValueType": { - "$ID": "7e14afd20e9a434da505f43544390480", + "$ID": "0f4497affdce5c48a8047f20d5953341", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2882,7 +905,7 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "true", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ 2 @@ -2896,7 +919,7 @@ "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": true, + "Required": false, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2906,19 +929,19 @@ "Translations": [ 2 ], - "Type": "Boolean" + "Type": "Action" } }, { - "$ID": "45d0d05e8bfa44fb9f9ca1b15601f4f2", + "$ID": "3dbb82ade4f97444b4489533ab384fce", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Store sort", - "Category": "Personalization::Configuration", + "Caption": "Filters", + "Category": "Filtering::Filtering", "Description": "", "IsDefault": false, - "PropertyKey": "storeSort", + "PropertyKey": "filterList", "ValueType": { - "$ID": "a6ce288bfa6040fb882d7cfaf3fbe247", + "$ID": "d6c2dbf8b357964ea3087cdffd1c90dd", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -2932,21 +955,85 @@ ], "DataSourceProperty": "", "DefaultType": "None", - "DefaultValue": "true", + "DefaultValue": "", "EntityProperty": "", "EnumerationValues": [ 2 ], "IsLinked": false, - "IsList": false, + "IsList": true, "IsMetaData": false, "IsPath": "No", "Multiline": false, - "ObjectType": null, + "ObjectType": { + "$ID": "312520d5cb22b243913c1e71b8d714d8", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "b205965aadfa6f4f981d9c93ef0fc4cf", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Filter attribute", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "filter", + "ValueType": { + "$ID": "1f758baa12fb9e43a7fd89da2317e35f", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "String", + "AutoNumber", + "Boolean", + "DateTime", + "Decimal", + "Enum", + "Integer", + "Long" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "../datasource", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + } + ] + }, "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", - "Required": true, + "Required": false, "ReturnType": null, "SelectableObjectsProperty": "", "SelectionTypes": [ @@ -2956,19 +1043,19 @@ "Translations": [ 2 ], - "Type": "Boolean" + "Type": "Object" } }, { - "$ID": "83f940d2d8e241ca814f598021dba1a9", + "$ID": "efbd4d6888ce054a924fb50226e83ae2", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Filter section", - "Category": "Accessibility::Aria labels", - "Description": "Assistive technology will read this upon reaching a filtering or sorting section.", + "Caption": "Filters placeholder", + "Category": "Filtering::Filtering", + "Description": "", "IsDefault": false, - "PropertyKey": "filterSectionTitle", + "PropertyKey": "filtersPlaceholder", "ValueType": { - "$ID": "e6046a4f671442d5a6f624d4b13fec78", + "$ID": "ea3d86661380c146a749d76a2e4d915e", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -3006,19 +1093,19 @@ "Translations": [ 2 ], - "Type": "TextTemplate" + "Type": "Widgets" } }, { - "$ID": "a94caf85279a41039ac8704c56ff144c", + "$ID": "7c37ac5f6a7d5447afd8c40efe1baafa", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Empty message", - "Category": "Accessibility::Aria labels", - "Description": "Assistive technology will read this upon reaching an empty message section.", + "Caption": "Sort attributes", + "Category": "Sorting::Sorting", + "Description": "", "IsDefault": false, - "PropertyKey": "emptyMessageTitle", + "PropertyKey": "sortList", "ValueType": { - "$ID": "86524910ce114f51974d6a0282d2b154", + "$ID": "efdedc7049df114fad52e23cf1aa2c40", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -3038,11 +1125,125 @@ 2 ], "IsLinked": false, - "IsList": false, + "IsList": true, "IsMetaData": false, "IsPath": "No", "Multiline": false, - "ObjectType": null, + "ObjectType": { + "$ID": "119613cd4c18cb45997bb0520e607f56", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "6a0d44bb9d055840a631114a7614c9a3", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Attribute", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "attribute", + "ValueType": { + "$ID": "acf0791dfabbd94f945291f5b74288bc", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "String", + "AutoNumber", + "Boolean", + "DateTime", + "Decimal", + "Enum", + "Integer", + "Long" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "../datasource", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "78174283d826f34dbbcb856d77167f7b", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Caption", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "caption", + "ValueType": { + "$ID": "6f67cf13691d4342984335b6116644bf", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + } + ] + }, "OnChangeProperty": "", "ParameterIsList": false, "PathType": "None", @@ -3056,19 +1257,19 @@ "Translations": [ 2 ], - "Type": "TextTemplate" + "Type": "Object" } }, { - "$ID": "f7f250cf1d13489a81e481cea8cb883b", + "$ID": "2c7377f3d522e248b5259b13d3a04503", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Content description", + "Caption": "Filter section", "Category": "Accessibility::Aria labels", - "Description": "Assistive technology will read this upon reaching gallery.", + "Description": "Assistive technology will read this upon reaching a filtering or sorting section.", "IsDefault": false, - "PropertyKey": "ariaLabelListBox", + "PropertyKey": "filterSectionTitle", "ValueType": { - "$ID": "617786fc63b7407cbfdb58763c59f035", + "$ID": "5d1d44fffcf2af4680267868fdb9cc81", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -3110,15 +1311,15 @@ } }, { - "$ID": "27d0cddf48cb423188d30c919e30e4fc", + "$ID": "8ed270589cb50c49b34aff9351eb0ac8", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Item description", + "Caption": "Empty message", "Category": "Accessibility::Aria labels", - "Description": "Assistive technology will read this upon reaching each gallery item.", + "Description": "Assistive technology will read this upon reaching an empty message section.", "IsDefault": false, - "PropertyKey": "ariaLabelItem", + "PropertyKey": "emptyMessageTitle", "ValueType": { - "$ID": "1e8cf5197fab423f9b8b32ff4e3f8cd5", + "$ID": "33482a1404aa4d42ac758ef42142ab45", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -3130,7 +1331,7 @@ "AssociationTypes": [ 1 ], - "DataSourceProperty": "datasource", + "DataSourceProperty": "", "DefaultType": "None", "DefaultValue": "", "EntityProperty": "", @@ -3160,15 +1361,15 @@ } }, { - "$ID": "b86b18c5971947d78d28751c71e3859f", + "$ID": "a93ee9d231b82f4c9124b44c6c865938", "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Item count singular", + "Caption": "Content description", "Category": "Accessibility::Aria labels", - "Description": "Must include '%d' to denote number position ('%d item selected')", + "Description": "Assistive technology will read this upon reaching gallery.", "IsDefault": false, - "PropertyKey": "selectedCountTemplateSingular", + "PropertyKey": "ariaLabelListBox", "ValueType": { - "$ID": "66908f0edb2b4b6c9d768de04f866d8e", + "$ID": "3e28d8e583685d45b13e1900c4983b47", "$Type": "CustomWidgets$WidgetValueType", "ActionVariables": [ 2 @@ -3208,67 +1409,5596 @@ ], "Type": "TextTemplate" } - }, - { - "$ID": "bbb8f2f283f3498d817401dafd1f7d0d", - "$Type": "CustomWidgets$WidgetPropertyType", - "Caption": "Item count plural", - "Category": "Accessibility::Aria labels", - "Description": "Must include '%d' to denote number position ('%d items selected')", - "IsDefault": false, - "PropertyKey": "selectedCountTemplatePlural", - "ValueType": { - "$ID": "71f1cbf825c74604a2b978fefd922903", - "$Type": "CustomWidgets$WidgetValueType", - "ActionVariables": [ + } + ] + }, + "OfflineCapable": true, + "StudioCategory": "Data Containers", + "StudioProCategory": "Data containers", + "SupportedPlatform": "Web", + "WidgetDescription": "", + "WidgetId": "com.mendix.widget.web.gallery.Gallery", + "WidgetName": "Gallery", + "WidgetNeedsEntityContext": false, + "WidgetPluginWidget": true + }, + "object": { + "$ID": "8c60e41d15cd504eb47c5333f340968e", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "8510467e7b3cb245b2b1ec6aa908cb3d", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "62b9b1d85ee7974fac2a80c72fbe646f", + "Value": { + "$ID": "941fb90b295d824f892f8c0845ebd741", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "cb5d3b4870201b40979ffe50d09be07b", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": { + "$ID": "9515c6b4bccb714995cfd9159b637bf1", + "$Type": "CustomWidgets$CustomWidgetXPathSource", + "EntityRef": { + "$ID": "03cb2d4562131e4eb1ce45787b7b54c4", + "$Type": "DomainModels$DirectEntityRef", + "Entity": "WorkflowCommons.MyInitiatedWorkflowView" + }, + "ForceFullObjects": false, + "SortBar": { + "$ID": "71e76d6aec7fe148b1a3520ac62ffb58", + "$Type": "Forms$GridSortBar", + "SortItems": [ + 2, + { + "$ID": "96691d1fdec0434994cff70da0bebc16", + "$Type": "Forms$GridSortItem", + "AttributeRef": { + "$ID": "dc1330f2b9f3eb43b3060ac5815fe1eb", + "$Type": "DomainModels$AttributeRef", + "Attribute": "WorkflowCommons.MyInitiatedWorkflowView.StartTime", + "EntityRef": null + }, + "SortOrder": "Descending" + } + ] + }, + "SourceVariable": null, + "XPathConstraint": "[WorkflowCommons.MyInitiatedWorkflowView_Initiator = '[%CurrentUser%]']" + }, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "41eee3044915484abe6fa417a2821162", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "63330f2428cef84fb27dfdd6def1fcb2", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3571441c44e92d46824f5b7faf9c6369", + "Value": { + "$ID": "ab938bafbd2400488dc118bcda0514be", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "c21006f648812a4696221b2ec873aec9", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "Single", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "37a3e452d97bf547ab6f09351e50b683", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "473bfb70496df940afe35d48dc9236e9", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "1c5af78035daad4c9771ab01956ae5fa", + "Value": { + "$ID": "3f52c33b18f27745b202e116c4976d0b", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5fa19ff84f7e2042925691c2a39e3c9f", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "b3862d42ae20bc4e8be9fde4097a4e29", + "Widgets": [ + 2, + { + "$ID": "b1bcb1a28a96cc42ae5797c992d0c32b", + "$Type": "Forms$LayoutGrid", + "Appearance": { + "$ID": "fc23d4cb8daf46448b203edfb588d9b4", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3, + { + "$ID": "16d588dbf3938c4fa37aa0f117050585", + "$Type": "Forms$DesignPropertyValue", + "Key": "Spacing", + "Value": { + "$ID": "70ac4ab73ab42648988f602e86bebf91", + "$Type": "Forms$CompoundDesignPropertyValue", + "Properties": [ + 2, + { + "$ID": "881a5d83f734b643b419b7cd7f5889f2", + "$Type": "Forms$DesignPropertyValue", + "Key": "padding-top", + "Value": { + "$ID": "5666d59adb95c54783154de1099fe00f", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "M" + } + }, + { + "$ID": "99485419e882b64f83e3e30c3ae88262", + "$Type": "Forms$DesignPropertyValue", + "Key": "padding-bottom", + "Value": { + "$ID": "9c4ef44de4b3ef43808511fa4dcc771e", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "M" + } + }, + { + "$ID": "562ad0849f5cf347ad5fe0f004e17b56", + "$Type": "Forms$DesignPropertyValue", + "Key": "padding-left", + "Value": { + "$ID": "01940f30ef4d894a83e50360f9640430", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "M" + } + }, + { + "$ID": "83b8da03818bed4fa4cb97acd06d8b43", + "$Type": "Forms$DesignPropertyValue", + "Key": "padding-right", + "Value": { + "$ID": "82eadab24788ce45b93a4820c998caa5", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "M" + } + } + ] + } + } + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalVisibilitySettings": null, + "Name": "layoutGrid3", + "Rows": [ + 2, + { + "$ID": "a49dddea46eff943ad8cd4f2dd93d6a5", + "$Type": "Forms$LayoutGridRow", + "Appearance": { + "$ID": "e104bd27b4f86542b6018f6ca03ba992", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "Columns": [ + 2, + { + "$ID": "271aa5cfba6a094ba26c79fbb2c3bf8d", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "dbcb54e17a43a146aede767e11af4c89", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -2, + "PreviewWidth": -1, + "TabletWeight": -2, + "VerticalAlignment": "Center", + "Weight": -2, + "Widgets": [ + 2, + { + "$ID": "7ffeeb0210f6e9479bd1f713b34725ea", + "$Type": "Forms$SnippetCallWidget", + "Appearance": { + "$ID": "6e459e62ef013f499ac8cc3bf77b3337", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "FormCall": { + "$ID": "5581499d6ef8804a94427dcaa75f5f3a", + "$Type": "Forms$SnippetCall", + "Form": "WorkflowCommons.Snip_MyInitiatedWorkflowView_StateCircleOnly", + "ParameterMappings": [ + 2 + ] + }, + "Name": "snippetCall5", + "TabIndex": 0 + } + ] + }, + { + "$ID": "fbe134d772ffe444a6154ad3e45ed49f", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "4123cab1bd042b42a06080672c9374f9", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -1, + "PreviewWidth": -1, + "TabletWeight": -1, + "VerticalAlignment": "Center", + "Weight": -1, + "Widgets": [ + 2, + { + "$ID": "5c74f18a1ded5e4b87ace3143307786f", + "$Type": "Forms$DynamicText", + "Appearance": { + "$ID": "5dde7a57916d974d9b929c8084f1e6d2", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3, + { + "$ID": "268477e620bcc54fae02a6c1d4a0f9d8", + "$Type": "Forms$DesignPropertyValue", + "Key": "Spacing", + "Value": { + "$ID": "a52de17c041db349b85e69481a2ddd75", + "$Type": "Forms$CompoundDesignPropertyValue", + "Properties": [ + 2, + { + "$ID": "7a761c884daef545b875f035170d079f", + "$Type": "Forms$DesignPropertyValue", + "Key": "margin-bottom", + "Value": { + "$ID": "5292873a893b7d4c8acc068a7a53f161", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "None" + } + } + ] + } + } + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalVisibilitySettings": null, + "Content": { + "$ID": "a85fd666197ce44eb8312388e4176e25", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "ffc91dd83e3b824ca3362c93702296b6", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2, + { + "$ID": "3258d348c3121b4fa36b5edbb96afb6f", + "$Type": "Forms$ClientTemplateParameter", + "AttributeRef": { + "$ID": "a59690f048facc4485a0ac1476bdd5cc", + "$Type": "DomainModels$AttributeRef", + "Attribute": "WorkflowCommons.MyInitiatedWorkflowView.Name", + "EntityRef": null + }, + "Expression": "", + "FormattingInfo": { + "$ID": "5768828577b52d46a225f5bbbfbde484", + "$Type": "Forms$FormattingInfo", + "CustomDateFormat": "", + "DateFormat": "Date", + "DecimalPrecision": 2, + "EnumFormat": "Text", + "GroupDigits": false + }, + "SourceVariable": null + } + ], + "Template": { + "$ID": "0f54dd2348958f4fa13302291b2360db", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "25c77fae009de84f900aad47b5aa6f72", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "{1}" + } + ] + } + }, + "Name": "text11", + "NativeAccessibilitySettings": null, + "NativeTextStyle": "Text", + "RenderMode": "H4", + "TabIndex": 0 + }, + { + "$ID": "790fbbc8ce6b1a4d8d6a97ade5e701b2", + "$Type": "Forms$DivContainer", + "Appearance": { + "$ID": "f68f3063e121064099eba8607fec1379", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalVisibilitySettings": null, + "Name": "container6", + "NativeAccessibilitySettings": null, + "OnClickAction": { + "$ID": "3acb8a1bf2d03046967dd4ff9d3d9a92", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "RenderMode": "Div", + "ScreenReaderHidden": false, + "TabIndex": 0, + "Widgets": [ + 2, + { + "$ID": "f4adc8a253281b4fa4a8b68b7be8fc3c", + "$Type": "Forms$DynamicText", + "Appearance": { + "$ID": "94c4a8bbade92146bc457148e80f0252", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalVisibilitySettings": null, + "Content": { + "$ID": "46b5491e2a559e439ba88fc67d622819", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "547b16270cc14f4ea38459296250ba89", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2, + { + "$ID": "82e77deacc8bed498c9552cbd26005c1", + "$Type": "Forms$ClientTemplateParameter", + "AttributeRef": { + "$ID": "f45babece315f34d80e37a7417118f9a", + "$Type": "DomainModels$AttributeRef", + "Attribute": "WorkflowCommons.MyInitiatedWorkflowView.StartTime", + "EntityRef": null + }, + "Expression": "", + "FormattingInfo": { + "$ID": "74beb37eb3de0d49b5b1acc46646f587", + "$Type": "Forms$FormattingInfo", + "CustomDateFormat": "", + "DateFormat": "DateTime", + "DecimalPrecision": 2, + "EnumFormat": "Text", + "GroupDigits": false + }, + "SourceVariable": null + } + ], + "Template": { + "$ID": "0f319ce7c5a99942a4a6f68cebd6eb3e", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "a3a4867f92968343909cb32ff813403e", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "{1}" + } + ] + } + }, + "Name": "text3", + "NativeAccessibilitySettings": null, + "NativeTextStyle": "Text", + "RenderMode": "Text", + "TabIndex": 0 + } + ] + } + ] + }, + { + "$ID": "8b9fb3a5f065b1479ee3f25c698a659f", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "04fec26738b9084693f96f2fe6ee7016", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -2, + "PreviewWidth": -1, + "TabletWeight": -2, + "VerticalAlignment": "Center", + "Weight": -2, + "Widgets": [ + 2, + { + "$ID": "7b7b066ce072e649ac3a7f88b73eabb2", + "$Type": "Forms$ActionButton", + "Action": { + "$ID": "fbcec5e3d60f9a4db6e1ef54f92d6a93", + "$Type": "Forms$CallNanoflowClientAction", + "ConfirmationInfo": null, + "DisabledDuringExecution": true, + "Nanoflow": "WorkflowCommons.ACT_DoNothing", + "OutputMappings": [ + 3 + ], + "ParameterMappings": [ + 2 + ], + "ProgressBar": "None", + "ProgressMessage": null + }, + "Appearance": { + "$ID": "f6644043cd37e14bb29ddef52aaa6a49", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "AriaRole": "Button", + "ButtonStyle": "Default", + "CaptionTemplate": { + "$ID": "f298c920f69319418f21646703c6ff75", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "6b5ccc707bc63c43bfed5b9002388fd1", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "6acb983fb7b7104a92de19d824bbac1a", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "ceb73772eafc6142bc795ba3d286cc1b", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "" + } + ] + } + }, + "ConditionalVisibilitySettings": null, + "Icon": { + "$ID": "e5a857fa71d7354fa1210155bcbeab66", + "$Type": "Forms$GlyphIcon", + "Code": 57944 + }, + "Name": "actionButton4", + "NativeAccessibilitySettings": null, + "RenderType": "Link", + "TabIndex": 0, + "Tooltip": { + "$ID": "ed50d71e78c9b9439ffb0ca42074068e", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "10f4a6e8f227e74f86485159ae8cf911", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Menu Right Icon" + } + ] + } + } + ] + } + ], + "ConditionalVisibilitySettings": null, + "HorizontalAlignment": "None", + "SpacingBetweenColumns": true, + "VerticalAlignment": "Center" + } + ], + "TabIndex": 0, + "Width": "FullWidth" + } + ], + "XPathConstraint": "" + } + }, + { + "$ID": "b720228030fe3644a10194ba85018135", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "f2dcfd0bba10874c95f47d4c45f4c526", + "Value": { + "$ID": "0585a54e17690f45953fbd591f82a9e0", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "bf7442df5777194d852e9c79ff52452e", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "1", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "8f68eb2f6aaed844a1e99f69043657c5", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "de6c8eb2f3e5a544ae551a9ec0cd8f04", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "358c6e49c4e54241abdd99f05965c006", + "Value": { + "$ID": "b97f71d68f19ba49a562272e752e936c", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f162ffde4ef8a142b9db7df6eb1a0ae9", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "1", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "c9a6cd6b5f1a3c4babef4b3e9baa3dab", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "3755268cc4d2584895781eb9975ede49", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "c38078fd298f9a40bd7dd8437d939f94", + "Value": { + "$ID": "8c1e6952318d874c81fa7aa926f3af82", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "ed39d216dea14b4ab8b1d33a6db231dd", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "1", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "f821313a6c7a1b4bb0f54c1fee5bda9d", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "23247fc4051e0743a96610b778241d05", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3f25c5443c8fa34ebe04588fa73341a9", + "Value": { + "$ID": "3bc2c25b1822ce44b4225a965bf753ca", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f91b18567e42b046b824b894e7bca37c", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "6", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "c532e4ea44ae0f4fba93ec27e700f296", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "77308b1bc3708646a9fdd0d356f89f7b", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "45bd939d9451034783b7967e55675118", + "Value": { + "$ID": "3ca4aba36c844146b9a1d5a34bd52adb", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f2d15d3307689943b73fecc2fa85b9a5", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "buttons", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "333d9ad699cc67459aeca95b8e9e94f0", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "9a02ee91d2b90444a4b4d5010b2f223e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "252d2c2006b6fc44a3256a32e457324b", + "Value": { + "$ID": "7ea80525e5b4604a94299510c72c93db", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "d305df428ca1a947ae9b2ca304f15a60", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "below", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "8fdab804f5be5c43960d3c1624d08888", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "afce1f7d6fd0f94d8c4220069d18045e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "ab11ac2ce566344488685887ae8c63bb", + "Value": { + "$ID": "e3ec3294306e4d4c9c226a09dc5fa29e", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "a3f8791d8fc1884b9f3b53bb84a62fa0", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "none", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "49e96d756d860d4eab9ed3d67e216504", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "f822e224118f6c40a1229e49fc98638b", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "367900ea5d5f674194f016300e4b96a4", + "Value": { + "$ID": "2fab441bd7d32e4c94892a9f6e17271b", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "883d82aa50840747ab0672fe0e812a7d", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "9e0cb79afeb06b4a85d3c19752c63f0d", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "1546bedc7c7dd241b1c6a4300e5e3222", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "446fb3a0fef0d5478e74926f27658b54", + "Value": { + "$ID": "393b53b12beea94a86db2f2a6a34d9e1", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "2bc17bd044702e47ac92fcf8daaf4d66", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "15f31411de0b784198d947dd87405377", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "bd07b728b46aa440a8e01ef52ea374b1", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "423f38a21ffc6141b3d9672257df1d93", + "Value": { + "$ID": "0cf2e122fb388641b89f00825ab35139", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "0dc6711005aa0840a1257207f026cf89", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "7e89bd0cbbb0cc4f863038fd7c4d23cc", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "5ed388f56ce24d4fbccac6e7366e6d97", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "daa185638007794fb7c3183593effadc", + "Value": { + "$ID": "d97400a29c9ab748bb1a20bbaba9cf2d", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "3a6b571f80ab084b80948ede531c7377", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "0f4497affdce5c48a8047f20d5953341", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "32e0485984e59442b5ffca38d66d9a61", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "efbd4d6888ce054a924fb50226e83ae2", + "Value": { + "$ID": "3627a52c8f3eb648852fe9ea6cabdc78", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5364dcc2c1189b418f357b580aa8463a", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "ea3d86661380c146a749d76a2e4d915e", + "Widgets": [ + 2, + { + "$ID": "4260aeda32a47242966f2f8263c262da", + "$Type": "Forms$LayoutGrid", + "Appearance": { + "$ID": "685b9f8b8e06ae4bae9e6a6f5356e493", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3, + { + "$ID": "0e57418d01204d4e85fa83572420ae63", + "$Type": "Forms$DesignPropertyValue", + "Key": "Spacing", + "Value": { + "$ID": "9c5d769eadc2a5429e80ad82bdf8dbae", + "$Type": "Forms$CompoundDesignPropertyValue", + "Properties": [ + 2, + { + "$ID": "e120b2e5086c364390b9d9bb5fc9d6ce", + "$Type": "Forms$DesignPropertyValue", + "Key": "margin-bottom", + "Value": { + "$ID": "e35863f14566e445b89959212a6ce0ea", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "S" + } + } + ] + } + } + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalVisibilitySettings": null, + "Name": "layoutGrid7", + "Rows": [ + 2, + { + "$ID": "f0fc4cf5ff1aff43877ae39f5d204c92", + "$Type": "Forms$LayoutGridRow", + "Appearance": { + "$ID": "2b40d56b3cfac64a82ed3f7fd19c7fa7", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "Columns": [ + 2, + { + "$ID": "3bf059c1495b5945aac590ddd3188f39", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "fd871ed4e360b44f8aa0b098af13b11b", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -1, + "PreviewWidth": -1, + "TabletWeight": -1, + "VerticalAlignment": "None", + "Weight": -1, + "Widgets": [ + 2, + { + "$ID": "f071d8b6ffee5a45bbcb66f0c4f4a366", + "$Type": "CustomWidgets$CustomWidget", + "Appearance": { + "$ID": "2829f051e27b5e4584dae41f131ae2e9", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalEditabilitySettings": null, + "ConditionalVisibilitySettings": null, + "Editable": "Always", + "LabelTemplate": null, + "Name": "drop_downFilter1", + "Object": { + "$ID": "59e30cb2c01a784a9361017a6176a82a", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "8fff43319f36c646a4c8c3b43b2c2616", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2cf79c1c69390440b82717035f9e02de", + "Value": { + "$ID": "79f4fc1920b87f4d88a1175579b24ee2", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "35279f830abe404aaec0240f76f9cc8e", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "ac6ce0aa628b1640b5ee0c869b00db32", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "7fa4bb2314ac1f4cbcc4ca737f688b63", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c894ace87331348accff561666cb17d", + "Value": { + "$ID": "dba977d0fb389c4e893dc8bfc4d58dea", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "48d88842a8743747aaf0fe05aa516b1a", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "36290ab2ed831548a4298b769835d941", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "a399d27213344a47a2792c3bbe62854e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "97ae465290c4d34883e6dea24767ad88", + "Value": { + "$ID": "09ed8c92c3ca2544a12603130237df6c", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5653f5e4a563344eb13c06ff7f9d73d8", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2, + { + "$ID": "f6639f841b1dea4d80b82a21955f4576", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "7ce9a68712e90f42b5366cca388c25de", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2a9c87368f4cc9479c6baa05e65da5dc", + "Value": { + "$ID": "71fc24f2d2ef4b48bd3e308b6ebd45ea", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "6f64e73142c88d4281b28aea6d2ccfc3", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "36d9b1ffb1841c4292355811c36cb284", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "cbd90c5ba34f72409cf28249fbc6917b", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "fe875263e2474240b58fa53bab073f12", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "79c27d35d8d352448603bb67b8124176", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show in progress" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "ae8fefed645c4a409910fd33a46bcb91", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "Value": { + "$ID": "18d25d3eff84414fb1e24e315a536cf6", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5ca508b1ec2a07498f241d7b564cda4a", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "getKey(System.WorkflowState.InProgress)", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "00ca928fb120cc4d95a0b62d05128345", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "ea8e6d7aa69b0a4294f8f905b01c74ad" + }, + { + "$ID": "2d88731bf53b4a4684309ace066af74e", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "faa258a8f50d724a97401aad293c1405", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2a9c87368f4cc9479c6baa05e65da5dc", + "Value": { + "$ID": "0981166387700844abd032a54c3d4698", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "09575b65bddfd64ab22613d2a8f5f889", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "254c8be9653f8948a8e03c53fa692f68", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "a2ef6b0429e39240b50a4e10d18f8329", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "10bf59b2713c2d47b4692494007c5063", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "5849b69efe8e924bbf760f13853b7b41", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show paused" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "7e5defa2093a964a9af0a4b734f7ad83", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "Value": { + "$ID": "0e37b20bb5c6184aa5d1d5299dd797a6", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "2ab69f9d2822b44095d4e49ccfd87861", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "getKey(System.WorkflowState.Paused)", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "00ca928fb120cc4d95a0b62d05128345", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "ea8e6d7aa69b0a4294f8f905b01c74ad" + }, + { + "$ID": "d5483acb17ba0240879ea73ccaaa17b3", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "27c1ae24ec8b6d4098d85e09105e064e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2a9c87368f4cc9479c6baa05e65da5dc", + "Value": { + "$ID": "9d352658ece0274d99eac387f4efcd5e", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "68bf34f4c82d16409247d605f8e12836", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "2383c9e85e0f2a408939829e057aa110", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "f63ab2b9174eb04db6071ca7044e0583", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "31c5f13fece5aa49bb3ad83e808e5f07", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "9fa1bdf4ec8ae94e815199c51e250c1d", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show completed" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "10def91d61eb10468d3405eed85ce0c5", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "Value": { + "$ID": "9dcc0e8135736a47b39b88578a6f8b07", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "37b44ff817e4504f80dcaadc53acdc70", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "getKey(System.WorkflowState.Completed)", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "00ca928fb120cc4d95a0b62d05128345", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "ea8e6d7aa69b0a4294f8f905b01c74ad" + }, + { + "$ID": "ff8f0cd9d76de74db76bf361f300ff58", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "9768b1e30fad46438691cb20a78d127c", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2a9c87368f4cc9479c6baa05e65da5dc", + "Value": { + "$ID": "2de6aa7edcfad546b866b76d30492f69", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f2eabd59c959a145ae8f92088090905a", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "3e084b25098a6f4999cd0b6a920a80bb", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "351e351d08291f46bdddbcb2170efd75", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "713fbecd8bd2534ba36846edbfefe8fd", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "313da8766b3d8444a1b767b16ce22035", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show aborted" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "d0ae50aebf31d94585ae34ecd79efbc1", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "Value": { + "$ID": "5cb73f59caea2342990d8793aadca820", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "096daba742dfef4eb205e6bdff7ffe8f", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "getKey(System.WorkflowState.Aborted)", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "00ca928fb120cc4d95a0b62d05128345", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "ea8e6d7aa69b0a4294f8f905b01c74ad" + }, + { + "$ID": "5c20c7ba476b224eaf54c6f85ddc5f0d", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "5f50779bb2b2194297656a229a37fe79", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2a9c87368f4cc9479c6baa05e65da5dc", + "Value": { + "$ID": "f0536025b6f00346bdf6dbd98f8a11a6", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "747f851c3b8b2b48bd8d9c02a0ad3344", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "cf93576ad9395345b0a08170e734b0b0", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "6d927ea08cea984589da41b89a566038", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "0597dc87415fe94abdca496494918c8e", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "6a94f52263976d43806b450e43a43a82", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show incompatible" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "8c0c2ee303fdc349a824207fc9c2473a", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "Value": { + "$ID": "49db46e7c82137409754a3be270504fc", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "6ad33a2d950791479a197bc267501176", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "getKey(System.WorkflowState.Incompatible)", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "00ca928fb120cc4d95a0b62d05128345", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "ea8e6d7aa69b0a4294f8f905b01c74ad" + } + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "188110f695d9ae48ae75830c5875aac9", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "02abd224df875c4e86a380cb9e2ab195", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "a845c44bc8d4af458b4b335e773ad415", + "Value": { + "$ID": "b78e87e60ff63a46880d5517b886b079", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "1e97f9fef5ef6f4ca299643515caa114", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "d700c6db84700c4fb2b539e0f6c361ad", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "aee450822b7a8e40b54735a853b82df7", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "7fb5c5f8f17b6642ad2bcbcadf492a5b", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "0064eae2a5ecbe4c953195f0f456fc35", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show all states" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "583bc91d6f45124a989e0fd5c5e68e58", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "6382d198fcd1c544bbb1bd3450dc28e3", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "8d21ac5577ec134da73ab0f0ab6190b4", + "Value": { + "$ID": "29a1edb42c35d04eb940f64d071b07cb", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "4aad42a35a8d1d46a94e31a7f5e56bc8", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "3903ab8ff44846449a3b46d461f93432", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "49d595e19f776d4eb8e574332bad62d9", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "b4babe1fabaee7488594c1d8013ec957", + "Value": { + "$ID": "c8b262e48e72374db1009555a7ceefe3", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "c9bc64019843df49877a72a785a8578d", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "c1a48fcec1fd9b47b32b8a5e1253cf85", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "62fdadd1e1ee4146b7fadc79497d11f2", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "f689473b8bf7984f9c4b47c5dd1ca6e1", + "Value": { + "$ID": "2f24e3aba9b9ce49ba010a802dc38d3a", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "4fd755f4b570c946871be1d5620a60a7", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "67e66dc3f4475648bb13d4de035dd2a1", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "a40c532e350b6c4c98b43c28d9344b70", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "c381715592592d40b187454f24a871f9", + "Value": { + "$ID": "ae2826e71dbf474bb3389bf19437bedb", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "fca1632d6db6bb4599e5bd6259e8ed12", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "c0bb6f9bb72a6048a3cb7dfe024251e7", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "62b42cca89db464ab3240789e4e32871", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "00d18e57a3b68140bdf185fea71760c5", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "7321e1e326b8df48b1170e5c757e04f5", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "43658574569c1e4dbbfe847b798ee245", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "8b54c1fbd237bd45ae5576e0c222d278", + "Value": { + "$ID": "a7662d7899d16244ae5573362403bf60", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5a7109624ab3f34da90c2928dc563cc5", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "d815ad119f699940ac276276eaea85b7", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "c3eea38acaddbe499df1241021d621ea" + }, + "TabIndex": 0, + "Type": { + "$ID": "37604580b22d5c41a12aefa5847849b3", + "$Type": "CustomWidgets$CustomWidgetType", + "HelpUrl": "https://docs.mendix.com/appstore/modules/data-grid-2#7-2-drop-down-filter", + "ObjectType": { + "$ID": "c3eea38acaddbe499df1241021d621ea", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "2cf79c1c69390440b82717035f9e02de", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Automatic options", + "Category": "General::General", + "Description": "Show options based on the references or the enumeration values and captions.", + "IsDefault": false, + "PropertyKey": "auto", + "ValueType": { + "$ID": "ac6ce0aa628b1640b5ee0c869b00db32", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "true", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "8b54c1fbd237bd45ae5576e0c222d278", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Enable advanced options", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "advanced", + "ValueType": { + "$ID": "d815ad119f699940ac276276eaea85b7", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "false", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "3c894ace87331348accff561666cb17d", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default value", + "Category": "General::General", + "Description": "Empty option caption will be shown by default or if configured default value matches none of the options", + "IsDefault": false, + "PropertyKey": "defaultValue", + "ValueType": { + "$ID": "36290ab2ed831548a4298b769835d941", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": { + "$ID": "d98c029b40053a4280f04a435467fc49", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "String" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + }, + { + "$ID": "97ae465290c4d34883e6dea24767ad88", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Options", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "filterOptions", + "ValueType": { + "$ID": "188110f695d9ae48ae75830c5875aac9", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": true, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": { + "$ID": "ea8e6d7aa69b0a4294f8f905b01c74ad", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "2a9c87368f4cc9479c6baa05e65da5dc", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Caption", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "caption", + "ValueType": { + "$ID": "4c0d6fb004ce9b4ab3a74dcbcb858302", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "3c1af1ccd65e6c459e46ad6bd0698b2b", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Value", + "Category": "General", + "Description": "", + "IsDefault": false, + "PropertyKey": "value", + "ValueType": { + "$ID": "00ca928fb120cc4d95a0b62d05128345", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": { + "$ID": "cfef7fa9da2c4a4bae7f62c60d863485", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "String" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + } + ] + }, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Object" + } + }, + { + "$ID": "a845c44bc8d4af458b4b335e773ad415", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Empty option caption", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "emptyOptionCaption", + "ValueType": { + "$ID": "583bc91d6f45124a989e0fd5c5e68e58", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "8d21ac5577ec134da73ab0f0ab6190b4", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Multiselect", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "multiSelect", + "ValueType": { + "$ID": "3903ab8ff44846449a3b46d461f93432", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "false", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "b4babe1fabaee7488594c1d8013ec957", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Saved attribute", + "Category": "General::Configurations", + "Description": "Attribute used to store the last value of the filter. Associations are not supported.", + "IsDefault": false, + "PropertyKey": "valueAttribute", + "ValueType": { + "$ID": "c1a48fcec1fd9b47b32b8a5e1253cf85", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "String" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "f689473b8bf7984f9c4b47c5dd1ca6e1", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "On change", + "Category": "General::Events", + "Description": "Action to be triggered when the value or filter changes.", + "IsDefault": false, + "PropertyKey": "onChange", + "ValueType": { + "$ID": "67e66dc3f4475648bb13d4de035dd2a1", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Action" + } + }, + { + "$ID": "c381715592592d40b187454f24a871f9", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Input caption", + "Category": "Accessibility::Accessibility", + "Description": "Assistive technology will read this upon reaching the input element.", + "IsDefault": false, + "PropertyKey": "ariaLabel", + "ValueType": { + "$ID": "7321e1e326b8df48b1170e5c757e04f5", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + } + ] + }, + "OfflineCapable": true, + "StudioCategory": "Data Controls", + "StudioProCategory": "Data controls", + "SupportedPlatform": "Web", + "WidgetDescription": "", + "WidgetId": "com.mendix.widget.web.datagriddropdownfilter.DatagridDropdownFilter", + "WidgetName": "Drop-down filter", + "WidgetNeedsEntityContext": false, + "WidgetPluginWidget": true + } + } + ] + } + ], + "ConditionalVisibilitySettings": null, + "HorizontalAlignment": "None", + "SpacingBetweenColumns": true, + "VerticalAlignment": "None" + }, + { + "$ID": "3651fd5ea16e8f408b6c818843b032ae", + "$Type": "Forms$LayoutGridRow", + "Appearance": { + "$ID": "8b500fe75284a548895661149a3aaed4", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "Columns": [ + 2, + { + "$ID": "b6332e3f0ae65245bc74d3f01e6ef0f3", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "2f27664614311b4a8fb2b7f2fff85fe9", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -1, + "PreviewWidth": -1, + "TabletWeight": -1, + "VerticalAlignment": "None", + "Weight": -1, + "Widgets": [ + 2, + { + "$ID": "13f6e641b63ab645a892e633354dbac9", + "$Type": "CustomWidgets$CustomWidget", + "Appearance": { + "$ID": "4be0174f2d0ce64cb6302b7943cc0e68", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3, + { + "$ID": "940d87abee3c624a9d36ee71362c9e4b", + "$Type": "Forms$DesignPropertyValue", + "Key": "Spacing", + "Value": { + "$ID": "d92a8c3e824a764da2a14326d716c735", + "$Type": "Forms$CompoundDesignPropertyValue", + "Properties": [ + 2, + { + "$ID": "38053d4c0793544981d7ecd67c3124bc", + "$Type": "Forms$DesignPropertyValue", + "Key": "margin-top", + "Value": { + "$ID": "2b94ecebf28010468a58d2eba9667239", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "S" + } + } + ] + } + } + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalEditabilitySettings": null, + "ConditionalVisibilitySettings": null, + "Editable": "Always", + "LabelTemplate": null, + "Name": "dateFilter1", + "Object": { + "$ID": "3175925645705b47a895b88baf20dca8", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "1cfbe80e0c9a804797faac0c22527db7", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "36a76a5aadd70b4c9f9fb338d2fa6384", + "Value": { + "$ID": "7d564963208d5343864ec51abfa3343b", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "2447d1cd7ffa6f40b151ca3d58275f48", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "1cceb38a62242441999a02f667e63f62", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "472478e9ce77944b82e8d01119efda25", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "c5a01e0e5d28174bba84164af61d5d32", + "Value": { + "$ID": "907c2fb5272855488eb697dc4fd06e62", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "6c2f7ad77b52d5458dcd30411702c7b4", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "9c7c93297576984794e37cb3b07af9be", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "5cc7f94ae1b7a344883a4eeac4b0026e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "a03db6a63c8fa2439b7d51a9bb7869a6", + "Value": { + "$ID": "ee1029e8b2c89f4f8e44a5e611512cfb", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "51eba027c70ccd4da689c7ef837fb9b6", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "eddf3a4dd82d894baacb80a9dc221837", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "2ac3d27616b9c74e828e33fa3061f0df", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "dc48f08fa32c5345a1758e103872d57c", + "Value": { + "$ID": "c2b58a08b5f89740b76785b8f8fb0eb9", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "e3b491c95c1ec643bc6cc1ac56322ec2", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "smaller", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "61aa2c4afee7a74bb03dcc9fb9db2112", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "f6474fcb67fcf644838c4f520428c3ff", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "53ffaac52d0c0a4ca637d4048d6e8c49", + "Value": { + "$ID": "8075a1376f21bf4997b7f0a7555b1cd0", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "17e8f709dcf7004588ca03e1202e4c33", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "f28c7f760ca4fb409b9e9f515f1f0927", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "4a8e20a73a8fca46825879e3140553eb", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "489f00ae63ae2b4b96f7a18cea8c498b", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "e43c54ce2cc7124d893e018ef8aa5741", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Show all time" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "b79a9a7809fd6547af0e5fbcff25d495", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "c56fcc334af3a34a918c12061e0a205e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "d7234ba08b216541a86ffb0203fa2759", + "Value": { + "$ID": "c0995dde937b7641a6ddf83bde5faea8", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "9dc1638c06448b4fad89b5d916b6b22d", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "true", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "6e0bd1aca20a804eaf5a6db597f50e44", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "988a84dcd0fc414b908d4dc0bf5605d1", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "97bb39fb5ca0f548a0cf43f9c3a449f8", + "Value": { + "$ID": "8720c22128253140a3cc4bb3c3085ecb", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "e220708853c3424e9d1e8e1e4fac85b2", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "eb4edbfbf5d18743873d0db76246ae0e", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "e6d96c5dd864ec45987b9cee048ca588", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "613f17007acc66499542b2a9b377d80d", + "Value": { + "$ID": "72254900a55f544fa060bd5c44a6b130", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "bf27e8da038f7e42b0c084d7fa241156", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "07e5c431449c9746be20ca7710228a62", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "37d568f87fa33f4ebd6e5c628ddb57d8", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "7435a0e4ec804140aa6c7b33c8f5bd0d", + "Value": { + "$ID": "f82e31476691e7459e2585d8a7f4d96c", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "c97e8d7f8c4f1749ae6caea7a24bb643", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "891ba05556fbae4f8d48ac04e7fbc861", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "6bf57f54670bc84d929788c42d5301e4", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "9bf53dea95b2d745bb5043b90e307c2a", + "Value": { + "$ID": "7096b76dcb3b7b48898696685d73b148", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "e80127af34bd074298d55ea5ccadfc45", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "71ea55edd7a8eb48ae9ec092aaa597a1", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "9fdb91c06520f4418a924ce6e7c1c86b", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "cb4da070a713b74089ded0a3c2e45bba", + "Value": { + "$ID": "1a5deb637269ff47bcd09646896f1261", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "db90c65de65ba34b94eb45666ca00c7a", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "64fb76416bba3645a939c179e1457778", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "2d968ca89dde0445bd4560a264654068", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "cdd2d9c821a00d439e2e29a6106c804c", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "563ebae60fcfe64395cbca292cc1958c", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "48840b2421595e498b4375036b2fab2e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "02970a7b96bd4f4cb730d3972751ad7c", + "Value": { + "$ID": "c52ec32ea6b1c944b14cb8bd8391563e", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "527df8d951fdc34c95a271160aad5bb7", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "a0c8303e049a2d42a5238d756f1b11b9", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "12a2441b3515e144994585b12f69cf72", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "9a09029412a5134ea43191b68dce19f6", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "e90cdfa62de7ac4faba2517400e34613", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "c52b072c1cd7d04b9a697830aacdcd47", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "03b641b6b96e37488d2943057aba4e7c", + "Value": { + "$ID": "1a2d0248e676714f904866da12f1cc03", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "437acf9d6dcdb44dae29136f0964f1e3", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "7e56ab94f984a14b8bb758db6a13ac01", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "2041b29d83a8ce44a1373af0ba21a6dd", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "63d444170ca135438bb703afe6dd50ee", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "dbaa001b369b8d489c9a78111293b9c5", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "647e1c43c0702943a978659ad3be11af", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "6ad4d1ec6de6104bb0b3bb85552e9b0b", + "Value": { + "$ID": "fab2c0637856f745833792194f7dcc5c", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "43b5b6b17d7c1249abcba3f8c79a3225", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "475deaae095e6e4e959e14a2b20e057d", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "19a1cbe8fc150745b6c1e765a0b9774b" + }, + "TabIndex": 0, + "Type": { + "$ID": "592c370e2d15e644b0ec441c61d2fed5", + "$Type": "CustomWidgets$CustomWidgetType", + "HelpUrl": "https://docs.mendix.com/appstore/modules/data-grid-2#7-1-date-filter", + "ObjectType": { + "$ID": "19a1cbe8fc150745b6c1e765a0b9774b", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "6ad4d1ec6de6104bb0b3bb85552e9b0b", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Enable advanced options", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "advanced", + "ValueType": { + "$ID": "475deaae095e6e4e959e14a2b20e057d", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "false", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "36a76a5aadd70b4c9f9fb338d2fa6384", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default value", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultValue", + "ValueType": { + "$ID": "1cceb38a62242441999a02f667e63f62", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": { + "$ID": "069cb65cbcecf246be912bf89cb59c0c", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "DateTime" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + }, + { + "$ID": "c5a01e0e5d28174bba84164af61d5d32", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default start date", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultStartDate", + "ValueType": { + "$ID": "9c7c93297576984794e37cb3b07af9be", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": { + "$ID": "ba51219c6ca35c47aee3f495974ca2fd", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "DateTime" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + }, + { + "$ID": "a03db6a63c8fa2439b7d51a9bb7869a6", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default end date", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultEndDate", + "ValueType": { + "$ID": "eddf3a4dd82d894baacb80a9dc221837", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": { + "$ID": "37c4360cb5c9424b894684fe4bc7de5f", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "DateTime" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + }, + { + "$ID": "dc48f08fa32c5345a1758e103872d57c", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default filter", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultFilter", + "ValueType": { + "$ID": "61aa2c4afee7a74bb03dcc9fb9db2112", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "equal", + "EntityProperty": "", + "EnumerationValues": [ + 2, + { + "$ID": "d201533a23eb254581e51fe0eba6ac44", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Between", + "_Key": "between" + }, + { + "$ID": "d8e46bc4205654448a569917e1ffe675", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Greater than", + "_Key": "greater" + }, + { + "$ID": "720b3d7837f9d344b2070d4e6252b4fc", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Greater than or equal", + "_Key": "greaterEqual" + }, + { + "$ID": "0acf125f5323aa478bd9d51b24e7fd1d", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Equal", + "_Key": "equal" + }, + { + "$ID": "e8e28d6114ef304691aaf5aae7cfa0d2", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Not equal", + "_Key": "notEqual" + }, + { + "$ID": "84507619183ef443b32d9b1365a6e423", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Smaller than", + "_Key": "smaller" + }, + { + "$ID": "6fb3721686e5864a9563497b0755c9b5", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Smaller than or equal", + "_Key": "smallerEqual" + }, + { + "$ID": "841fea1ce5917f4e8aafa2639888057f", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Empty", + "_Key": "empty" + }, + { + "$ID": "e419b02ba4be2f40b62bf06205475a0d", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Not empty", + "_Key": "notEmpty" + } + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Enumeration" + } + }, + { + "$ID": "53ffaac52d0c0a4ca637d4048d6e8c49", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Placeholder", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "placeholder", + "ValueType": { + "$ID": "b79a9a7809fd6547af0e5fbcff25d495", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "d7234ba08b216541a86ffb0203fa2759", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Adjustable by user", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "adjustable", + "ValueType": { + "$ID": "6e0bd1aca20a804eaf5a6db597f50e44", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "true", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "97bb39fb5ca0f548a0cf43f9c3a449f8", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Saved attribute", + "Category": "General::Configurations", + "Description": "Attribute used to store the last value of the filter.", + "IsDefault": false, + "PropertyKey": "valueAttribute", + "ValueType": { + "$ID": "eb4edbfbf5d18743873d0db76246ae0e", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "DateTime" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "613f17007acc66499542b2a9b377d80d", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Saved start date attribute", + "Category": "General::Configurations", + "Description": "Attribute used to store the last value of the start date filter.", + "IsDefault": false, + "PropertyKey": "startDateAttribute", + "ValueType": { + "$ID": "07e5c431449c9746be20ca7710228a62", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "DateTime" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "7435a0e4ec804140aa6c7b33c8f5bd0d", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Saved end date attribute", + "Category": "General::Configurations", + "Description": "Attribute used to store the last value of the end date filter.", + "IsDefault": false, + "PropertyKey": "endDateAttribute", + "ValueType": { + "$ID": "891ba05556fbae4f8d48ac04e7fbc861", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "DateTime" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "9bf53dea95b2d745bb5043b90e307c2a", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "On change", + "Category": "General::Events", + "Description": "Action to be triggered when the value or filter changes.", + "IsDefault": false, + "PropertyKey": "onChange", + "ValueType": { + "$ID": "71ea55edd7a8eb48ae9ec092aaa597a1", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Action" + } + }, + { + "$ID": "cb4da070a713b74089ded0a3c2e45bba", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Comparison button caption", + "Category": "Accessibility::Screen reader", + "Description": "Assistive technology will read this upon reaching the comparison button that triggers the filter type drop-down menu.", + "IsDefault": false, + "PropertyKey": "screenReaderButtonCaption", + "ValueType": { + "$ID": "563ebae60fcfe64395cbca292cc1958c", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "02970a7b96bd4f4cb730d3972751ad7c", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Calendar button caption", + "Category": "Accessibility::Screen reader", + "Description": "Assistive technology will read this upon reaching the button that triggers the calendar.", + "IsDefault": false, + "PropertyKey": "screenReaderCalendarCaption", + "ValueType": { + "$ID": "e90cdfa62de7ac4faba2517400e34613", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "03b641b6b96e37488d2943057aba4e7c", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Input caption", + "Category": "Accessibility::Screen reader", + "Description": "Assistive technology will read this upon reaching the input element.", + "IsDefault": false, + "PropertyKey": "screenReaderInputCaption", + "ValueType": { + "$ID": "dbaa001b369b8d489c9a78111293b9c5", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + } + ] + }, + "OfflineCapable": true, + "StudioCategory": "Data Controls", + "StudioProCategory": "Data controls", + "SupportedPlatform": "Web", + "WidgetDescription": "", + "WidgetId": "com.mendix.widget.web.datagriddatefilter.DatagridDateFilter", + "WidgetName": "Date filter", + "WidgetNeedsEntityContext": false, + "WidgetPluginWidget": true + } + } + ] + } + ], + "ConditionalVisibilitySettings": null, + "HorizontalAlignment": "None", + "SpacingBetweenColumns": true, + "VerticalAlignment": "None" + }, + { + "$ID": "0c44e96bec12fe4fa13379fa7dc5ec5c", + "$Type": "Forms$LayoutGridRow", + "Appearance": { + "$ID": "90d9b00b61e9eb43bf8f56ee0ae90e86", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "Columns": [ + 2, + { + "$ID": "dbf9b697be02d84e952a4c3b6aa2cce8", + "$Type": "Forms$LayoutGridColumn", + "Appearance": { + "$ID": "23ddbea9a33fee458d9a59dc9c4220a8", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3 + ], + "DynamicClasses": "", + "Style": "" + }, + "PhoneWeight": -1, + "PreviewWidth": -1, + "TabletWeight": -1, + "VerticalAlignment": "None", + "Weight": -1, + "Widgets": [ + 2, + { + "$ID": "566452bf6eef2844a0f91400c72ecf69", + "$Type": "CustomWidgets$CustomWidget", + "Appearance": { + "$ID": "68ab34a99c04d7449c2776114b0374f4", + "$Type": "Forms$Appearance", + "Class": "", + "DesignProperties": [ + 3, + { + "$ID": "d55b3acd8432794bb9e62fed42846486", + "$Type": "Forms$DesignPropertyValue", + "Key": "Spacing", + "Value": { + "$ID": "52939f5653b63c4686847ce754cc6163", + "$Type": "Forms$CompoundDesignPropertyValue", + "Properties": [ + 2, + { + "$ID": "7155e5579a1f1a4ca1f76fc0c52e3df3", + "$Type": "Forms$DesignPropertyValue", + "Key": "margin-top", + "Value": { + "$ID": "4ea63a90a96d7e469aea1206ff07bcbd", + "$Type": "Forms$OptionDesignPropertyValue", + "Option": "S" + } + } + ] + } + } + ], + "DynamicClasses": "", + "Style": "" + }, + "ConditionalEditabilitySettings": null, + "ConditionalVisibilitySettings": null, + "Editable": "Always", + "LabelTemplate": null, + "Name": "textFilter1", + "Object": { + "$ID": "bf8bcf1e18e780469ef6cc3f0fd8e67a", + "$Type": "CustomWidgets$WidgetObject", + "Properties": [ + 2, + { + "$ID": "5d0b46bfd392fd4dbe6b012a00c4beec", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "94a78ddae7dc4646949c0d3ab08d3b6f", + "Value": { + "$ID": "4ab7f2a1e0a62641951ead2e520cd7d8", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "afdf8f5fd282184886835710460e4443", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "44c6a8a9608f7745bc7d45b8a30a91ca", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "41409be52681b4438f12934e9267c27c", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "e38c3e35b7ac8e41a68436b0cb0a0a0a", + "Value": { + "$ID": "f73e91ffab1ea5468b9d172018c72729", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "b9d515e1df55b646906c959d3b35c7dd", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "contains", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "a3bfa1a533e4614e97dbeff6e8a8654e", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "e73b6135e89eee44b7be8804248aa0ed", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "d6a7129aa880864cadecade3ac529df8", + "Value": { + "$ID": "4bc6d174e8f70a4d931d683046f8d8ce", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "92cdf98cc02b684d86fce2226fda3177", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "81f692d04f11a447ab4dcd271698711c", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "a878392c0cc8c5428d4ef9410a8783cb", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "84c0426a4e57134482cef10dbed8faba", + "$Type": "Texts$Text", + "Items": [ + 3, + { + "$ID": "4297be3286ef504f82d1bc43ac9a070b", + "$Type": "Texts$Translation", + "LanguageCode": "en_US", + "Text": "Search" + } + ] + } + }, + "TranslatableValue": null, + "TypePointer": "7294fa676590dd4fa3b03829c643a51a", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "1dbe1fce861b9d48a7c39f49083d2e99", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "df9531b94aeb1e4fbb54063149ea1ae1", + "Value": { + "$ID": "2611b250788b884087ae14984cfa3c32", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "a3b2f0a61cdd7442a861bc5f2706bfc3", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "true", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "4986156de74bc1468c1ebb0f29c046fb", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "876d02b5f8d2024b8436d37cfe444e16", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "ca3960e793a7e2458988161850009723", + "Value": { + "$ID": "20ca055dd63a2c4cadf7dfc70874daf8", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "edda2fb00071ab498311fa1dafb3e9fc", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "500", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "340adaeb498fc94cbc40eb9046bef2dc", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "a0117c9b27184248a351559a0be4fc52", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "f412d933f87af94eae3bce25bc8217ae", + "Value": { + "$ID": "f0df9c6dad0ab84f9eefae46be364493", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "9197f548ade295429bf2571b6935f123", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "3ed48eaa5342c141bbb614d5e516015e", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "32770d9cf382d44a87a268acb3fe425e", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "d49894c65e78674abf0f69dee6b94d07", + "Value": { + "$ID": "9fe149c5b6d1f74b89e632e85657f824", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "305516174bd2bb43bba72437a5dee912", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "4335908ed66583498c9ba9e81b94c2b1", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "0e0331751a278045b992f8d7fe34af1f", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "59820d37b227fb4695a3ca2e520b8705", + "Value": { + "$ID": "1e13651965e6f445955964bc98ca2977", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "68b4768b9b6db549906ce2d795545a9b", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "7baa0420caa21542bc024fcc630cd3b9", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "e428a9db44061a48a36666e7329c294d", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "f61ef8f6542adc449e6b98627a1ac076", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "3cf8723353d997418000f6495e23159f", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "d3c99a618c6cb14583624965118f231c", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "8e6ce4d56c95674aab1302ac38f0e4af", + "Value": { + "$ID": "51ed739a07cc224da5985da4d4958b5d", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "98a87781dfc4cc468d56611f13d7f97e", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "7a6e61dcce10d343a9f7bd2051693e9c", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "77bcf59193354647877585cd0c246862", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ + 2 + ], + "Template": { + "$ID": "3bba27361887ca4cabd774f3ddfbaf49", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "0afb9faa3bb3ad42b676034d9864581f", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "6147e9117b6a724a9963d59a9abe1efb", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "a989a8d1ee5c584a88699af482be64fa", + "Value": { + "$ID": "b28f599fef42f546a7e6d211433f3e04", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "51431091db8314438c0c18998aa563fe", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "39b63e0e67ada847af2f697ec4ad5d0e", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "2a3d7df091295b4ab8813b61b366d0ed" + }, + "TabIndex": 0, + "Type": { + "$ID": "b76f53a260b82344850553fb4653c2e7", + "$Type": "CustomWidgets$CustomWidgetType", + "HelpUrl": "https://docs.mendix.com/appstore/modules/data-grid-2#7-4-text-filter", + "ObjectType": { + "$ID": "2a3d7df091295b4ab8813b61b366d0ed", + "$Type": "CustomWidgets$WidgetObjectType", + "PropertyTypes": [ + 2, + { + "$ID": "a989a8d1ee5c584a88699af482be64fa", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Enable advanced options", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "advanced", + "ValueType": { + "$ID": "39b63e0e67ada847af2f697ec4ad5d0e", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "false", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "94a78ddae7dc4646949c0d3ab08d3b6f", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default value", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultValue", + "ValueType": { + "$ID": "44c6a8a9608f7745bc7d45b8a30a91ca", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": { + "$ID": "1a0437c5b2be0843a34388ec513582de", + "$Type": "CustomWidgets$WidgetReturnType", + "AssignableTo": "", + "EntityProperty": "", + "IsList": false, + "Type": "String" + }, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Expression" + } + }, + { + "$ID": "e38c3e35b7ac8e41a68436b0cb0a0a0a", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Default filter", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "defaultFilter", + "ValueType": { + "$ID": "a3bfa1a533e4614e97dbeff6e8a8654e", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "contains", + "EntityProperty": "", + "EnumerationValues": [ + 2, + { + "$ID": "2d19a0e9069d5546aad010069e5d3782", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Contains", + "_Key": "contains" + }, + { + "$ID": "ef15411988bb5040b38bd11dc53bd93f", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Starts with", + "_Key": "startsWith" + }, + { + "$ID": "a7e827265c063f40b13a55901208ff4a", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Ends with", + "_Key": "endsWith" + }, + { + "$ID": "4fb495efeafea242b046ed1bdcf2e9b2", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Greater than", + "_Key": "greater" + }, + { + "$ID": "54c96599b31c84468a736db5c2c0134b", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Greater than or equal", + "_Key": "greaterEqual" + }, + { + "$ID": "6d8d1405d927a7458a44ab487ea93cb9", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Equal", + "_Key": "equal" + }, + { + "$ID": "fd9d76c18a1c484cb9995e08a0ba4efd", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Not equal", + "_Key": "notEqual" + }, + { + "$ID": "496b1383da3c3a44a89b48a3e9990856", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Smaller than", + "_Key": "smaller" + }, + { + "$ID": "fd5326735d85a341806bbf7fde4d0ce3", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Smaller than or equal", + "_Key": "smallerEqual" + }, + { + "$ID": "56a95d40c7ecdc4f913e60a940f257a3", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Empty", + "_Key": "empty" + }, + { + "$ID": "7c983b98b124074a97dab900fe0a127e", + "$Type": "CustomWidgets$WidgetEnumerationValue", + "Caption": "Not empty", + "_Key": "notEmpty" + } + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Enumeration" + } + }, + { + "$ID": "d6a7129aa880864cadecade3ac529df8", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Placeholder", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "placeholder", + "ValueType": { + "$ID": "7294fa676590dd4fa3b03829c643a51a", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "df9531b94aeb1e4fbb54063149ea1ae1", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Adjustable by user", + "Category": "General::General", + "Description": "", + "IsDefault": false, + "PropertyKey": "adjustable", + "ValueType": { + "$ID": "4986156de74bc1468c1ebb0f29c046fb", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "true", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Boolean" + } + }, + { + "$ID": "ca3960e793a7e2458988161850009723", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Apply after (ms)", + "Category": "General::On change behavior", + "Description": "Wait this period before applying then change(s) to the filter", + "IsDefault": false, + "PropertyKey": "delay", + "ValueType": { + "$ID": "340adaeb498fc94cbc40eb9046bef2dc", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "500", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": true, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Integer" + } + }, + { + "$ID": "f412d933f87af94eae3bce25bc8217ae", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Saved attribute", + "Category": "General::Configurations", + "Description": "Attribute used to store the last value of the filter.", + "IsDefault": false, + "PropertyKey": "valueAttribute", + "ValueType": { + "$ID": "3ed48eaa5342c141bbb614d5e516015e", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1, + "String", + "HashString" + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Attribute" + } + }, + { + "$ID": "d49894c65e78674abf0f69dee6b94d07", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "On change", + "Category": "General::Events", + "Description": "Action to be triggered when the value or filter changes.", + "IsDefault": false, + "PropertyKey": "onChange", + "ValueType": { + "$ID": "4335908ed66583498c9ba9e81b94c2b1", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "Action" + } + }, + { + "$ID": "59820d37b227fb4695a3ca2e520b8705", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Comparison button caption", + "Category": "Accessibility::Screen reader", + "Description": "Assistive technology will read this upon reaching the comparison button that triggers the filter type drop-down menu.", + "IsDefault": false, + "PropertyKey": "screenReaderButtonCaption", + "ValueType": { + "$ID": "3cf8723353d997418000f6495e23159f", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + }, + { + "$ID": "8e6ce4d56c95674aab1302ac38f0e4af", + "$Type": "CustomWidgets$WidgetPropertyType", + "Caption": "Input caption", + "Category": "Accessibility::Screen reader", + "Description": "Assistive technology will read this upon reaching the input element.", + "IsDefault": false, + "PropertyKey": "screenReaderInputCaption", + "ValueType": { + "$ID": "0afb9faa3bb3ad42b676034d9864581f", + "$Type": "CustomWidgets$WidgetValueType", + "ActionVariables": [ + 2 + ], + "AllowNonPersistableEntities": false, + "AllowedTypes": [ + 1 + ], + "AssociationTypes": [ + 1 + ], + "DataSourceProperty": "", + "DefaultType": "None", + "DefaultValue": "", + "EntityProperty": "", + "EnumerationValues": [ + 2 + ], + "IsLinked": false, + "IsList": false, + "IsMetaData": false, + "IsPath": "No", + "Multiline": false, + "ObjectType": null, + "OnChangeProperty": "", + "ParameterIsList": false, + "PathType": "None", + "Required": false, + "ReturnType": null, + "SelectableObjectsProperty": "", + "SelectionTypes": [ + 1 + ], + "SetLabel": false, + "Translations": [ + 2 + ], + "Type": "TextTemplate" + } + } + ] + }, + "OfflineCapable": true, + "StudioCategory": "Data Controls", + "StudioProCategory": "Data controls", + "SupportedPlatform": "Web", + "WidgetDescription": "", + "WidgetId": "com.mendix.widget.web.datagridtextfilter.DatagridTextFilter", + "WidgetName": "Text filter", + "WidgetNeedsEntityContext": false, + "WidgetPluginWidget": true + } + } + ] + } + ], + "ConditionalVisibilitySettings": null, + "HorizontalAlignment": "None", + "SpacingBetweenColumns": true, + "VerticalAlignment": "None" + } + ], + "TabIndex": 0, + "Width": "FullWidth" + } + ], + "XPathConstraint": "" + } + }, + { + "$ID": "65e23f37019e9b4baa4385730278dd12", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "2c7377f3d522e248b5259b13d3a04503", + "Value": { + "$ID": "fc91f44d8b3f8f43b35af9e87f871f1e", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "5559ecb288ec4648848160c1ab57d716", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "d7b705f44e050b47977c884d213e8d1d", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "82399ae161493c4b874ec36b57ec9040", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ 2 ], - "AllowNonPersistableEntities": false, - "AllowedTypes": [ - 1 - ], - "AssociationTypes": [ - 1 - ], - "DataSourceProperty": "", - "DefaultType": "None", - "DefaultValue": "", - "EntityProperty": "", - "EnumerationValues": [ + "Template": { + "$ID": "264384fdbf4f6646a0eff864e07ef8a4", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "5d1d44fffcf2af4680267868fdb9cc81", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "b4b8dc2b6b393d43817f81da8dd72e24", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "8ed270589cb50c49b34aff9351eb0ac8", + "Value": { + "$ID": "0ba5495e8cc60743baa73644397a1129", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f82f947d6a669d41bc89d0f56f6bb12f", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "bcf0016702e4e947997035981ae51d26", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "08b45447541fdd4194e32f32fc7c1289", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ 2 ], - "IsLinked": false, - "IsList": false, - "IsMetaData": false, - "IsPath": "No", - "Multiline": false, - "ObjectType": null, - "OnChangeProperty": "", - "ParameterIsList": false, - "PathType": "None", - "Required": false, - "ReturnType": null, - "SelectableObjectsProperty": "", - "SelectionTypes": [ - 1 - ], - "SetLabel": false, - "Translations": [ + "Template": { + "$ID": "0986a8205fe1914c9f9c57c7fddfc9c7", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "33482a1404aa4d42ac758ef42142ab45", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "7774c9d5ccd481499b856f6a63926d76", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "a93ee9d231b82f4c9124b44c6c865938", + "Value": { + "$ID": "45a8e6cdbf07e64783a70bb8c030d846", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "f2fc6f368a24144488abffdb40efc258", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": { + "$ID": "e79d50973217ee4a8fb20320ed61afeb", + "$Type": "Forms$ClientTemplate", + "Fallback": { + "$ID": "1bfab3ec1533ad4698f75a9b681ee6e4", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + }, + "Parameters": [ 2 ], - "Type": "TextTemplate" - } + "Template": { + "$ID": "ddc7b31b7f77a947a99eed63ba59f3d8", + "$Type": "Texts$Text", + "Items": [ + 3 + ] + } + }, + "TranslatableValue": null, + "TypePointer": "3e28d8e583685d45b13e1900c4983b47", + "Widgets": [ + 2 + ], + "XPathConstraint": "" } - ] - }, - "OfflineCapable": true, - "StudioCategory": "Data Containers", - "StudioProCategory": "Data containers", - "SupportedPlatform": "Web", - "WidgetDescription": "", - "WidgetId": "com.mendix.widget.web.gallery.Gallery", - "WidgetName": "Gallery", - "WidgetNeedsEntityContext": false, - "WidgetPluginWidget": true + }, + { + "$ID": "b22d18bd19349d438ee75eb8e4e4d079", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "0b4a1ed0814e814ab90f9bbc420680d3", + "Value": { + "$ID": "7fa4b17b11a458418a45a9fb89652258", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "ecb38e2f9abf9848902d7a5b0f611874", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "clear", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "e567dba05dcd134c8c8a40f975d428d5", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "d9a3fc44cb307e439838cc717e81d24b", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "a1118376074b0a4c940b1c24102d1c96", + "Value": { + "$ID": "1ce3d03832d3fa4b9b71f3f9d965856e", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "0b0e21a8d0e12e46bc84c098c04a08bf", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "single", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "b1b92672f016e344bc9ba41b78e70eb7", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "e8584f054bb3074db6f77636c2917830", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "f4297805fc0a2844b8d8e770ab0ab185", + "Value": { + "$ID": "bd2197a8bded60438ed47e0a255d431a", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "b5ee2b90bf146e43b978ecf293673cfb", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "false", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "9dd44ee2b43dd349bab35621e78c4211", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "16f5a1488ac8524cab9ee3004f132001", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "3dbb82ade4f97444b4489533ab384fce", + "Value": { + "$ID": "faf9a6fa2651404b9dc9b594f909eadc", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "875f3e6bda31b343823f3428d051b996", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "d6c2dbf8b357964ea3087cdffd1c90dd", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + }, + { + "$ID": "b3cecffb4d76a94b893d60a94ddf77cb", + "$Type": "CustomWidgets$WidgetProperty", + "TypePointer": "7c37ac5f6a7d5447afd8c40efe1baafa", + "Value": { + "$ID": "642f97d3b299364fa2098f4084220379", + "$Type": "CustomWidgets$WidgetValue", + "Action": { + "$ID": "8893c1faf8d41a4b8f99ac7b73d26708", + "$Type": "Forms$NoAction", + "DisabledDuringExecution": true + }, + "AttributeRef": null, + "DataSource": null, + "EntityRef": null, + "Expression": "", + "Form": "", + "Icon": null, + "Image": "", + "Microflow": "", + "Nanoflow": "", + "Objects": [ + 2 + ], + "PrimitiveValue": "", + "Selection": "None", + "SourceVariable": null, + "TextTemplate": null, + "TranslatableValue": null, + "TypePointer": "efdedc7049df114fad52e23cf1aa2c40", + "Widgets": [ + 2 + ], + "XPathConstraint": "" + } + } + ], + "TypePointer": "25db8bdc0d4afb4382846d3d22271efd" } } \ No newline at end of file