Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/skills/mendix/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,26 @@ container card1 (OnClick: microflow MyModule.ACT_OpenDetails, class: 'clickable-
This maps to the Mendix Container's **On click** event (`Forms$DivContainer.OnClickAction`).
A container with no `OnClick:`/`Action:` is non-clickable (a no-op action), exactly as in Studio Pro.

**Prefer a clickable container over an `actionbutton` when the trigger needs child
widgets or a context argument.** An `actionbutton` can't contain child widgets and
maps only page-context object parameters — so a tile showing a digit over a label,
or a card that opens a specific object, is best built as a `container` with
`OnClick:`. The container's `OnClick:` takes the same `(Param: …)` argument syntax
as an `actionbutton`'s `action:`:

```sql
-- Rich, parameterised trigger: a card that opens the object it represents
container tileCard (OnClick: microflow MyModule.ACT_Open(Item: $currentObject), class: 'tile') {
dynamictext tileValue (content: '4')
dynamictext tileLabel (content: '4 LEFT', class: 'tile-label')
}
```

Note the Mendix limit this works around: a button (or `OnClick`) can map **object**
parameters from the page context but **cannot pass a literal argument**. To vary a
literal per trigger (e.g. a 1–9 number pad), make one real microflow and a thin
wrapper per value (`ACT_Set1`…`ACT_Set9`), each calling the shared implementation.

### FOOTER Widget

Container for form action buttons:
Expand Down
14 changes: 13 additions & 1 deletion .claude/skills/mendix/generate-domain-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ Mendix supports four built-in auditing properties on persistent entities. Declar
* Order with full audit trail
*/
create persistent entity Sales.Order (
OrderNumber: autonumber,
OrderNumber: autonumber default 1,
TotalAmount: decimal not null,
status: enumeration(Sales.OrderStatus) not null,
owner: autoowner,
Expand Down Expand Up @@ -554,6 +554,18 @@ create persistent entity Module.Address (...);
create persistent entity Module.Order (...);
```

## Modelling for the client: widgets bind members, not expressions

A page widget binds an **attribute or association path** — never an expression. There
is no `substring(attr, i, 1)` or computed binding in a widget. So a value that must be
rendered per-part (each character of a code, each cell of a grid, each of N pencil
marks) has to be stored as **separate attributes**, not packed into one string and
indexed client-side. Model the wide form when the UI needs to address the parts
individually (e.g. `N1`…`N9` booleans rather than a packed `Notes` string); reach for
a computed/derived value only where a microflow or view-entity OQL produces it into a
real attribute. (Same reason the bucket-class idiom exists — see
`migrate-design-prototype.md`.)

## Documentation Best Practices

### Entity Documentation
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/java-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ begin
EmailAddress = $customer/Email
);

if not $isValid then
if not($isValid) then
validation feedback $customer/Email
message 'Please enter a valid email address';
end if;
Expand Down
13 changes: 7 additions & 6 deletions .claude/skills/mendix/mdl-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ create persistent entity Module.Customer (
-- Enumeration
status: Module.CustomerStatus default Active,

-- Auto-number
CustomerNumber: autonumber
-- Auto-number (REQUIRES a seed via `default N` — without it the build fails
-- CE7247 "Value cannot be empty")
CustomerNumber: autonumber default 1
);
/
```
Expand Down Expand Up @@ -73,7 +74,7 @@ create non-persistent entity Module.CustomerSearchParams (
| DateTime | `Name: datetime` | `CreatedAt: datetime` |
| Date | `Name: date` | `BirthDate: date` |
| Enumeration | `Name: Module.EnumName` | `status: Module.Status` |
| AutoNumber | `Name: autonumber` | `Code: autonumber` |
| AutoNumber | `Name: autonumber default 1` | `Code: autonumber default 1` (seed required) |
| Binary | `Name: binary` | `FileData: binary` |
| Hashed String | `Name: hashedstring` | `password: hashedstring` |

Expand Down Expand Up @@ -253,8 +254,8 @@ create persistent entity Module.Product (
Category: string(50),
Price: decimal
)
index idx_product_code on (Code)
index idx_product_category on (Category);
index idx_product_code (Code)
index idx_product_category (Category);
/
```

Expand Down Expand Up @@ -293,7 +294,7 @@ create persistent entity Shop.Product (

-- Order entity
create persistent entity Shop.Order (
OrderNumber: autonumber,
OrderNumber: autonumber default 1,
OrderDate: datetime not null,
status: Shop.OrderStatus default Draft,
TotalAmount: decimal,
Expand Down
33 changes: 29 additions & 4 deletions .claude/skills/mendix/migrate-design-prototype.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ spacing, font, component shapes. Do not invent styling the prototype doesn't sho

## Where the Theme Lives (read this first — it avoids the main friction)

- **Custom styles go inline in `theme/web/main.scss`, AFTER the `@import`s.** mxcli/the
agent generally **cannot create new SCSS partials** (only existing files are writable),
and styles placed after the imports win the cascade over Atlas defaults. Put all your
design tokens and component classes there.
- **Custom styles go in `theme/web/main.scss` AFTER the `@import`s, or in your own
partial.** Styles placed after the imports win the cascade over Atlas defaults. Once
`main.scss` grows, prefer splitting a partial out for readability: create
`theme/web/_<name>.scss` and add `@import "<name>";` after the Atlas imports (the same
cascade-order rule then applies within the partial). New partials **are** creatable —
keep the import order (custom after Atlas) and everything works.
- Use a **project prefix** for every custom class and CSS variable so they never collide
with Atlas or widget CSS. This project uses `ss-` (e.g. `.ss-panel`, `--ss-primary`).
Pick one prefix and use it everywhere.
Expand Down Expand Up @@ -416,6 +418,29 @@ container heatCell (

(Note the doubled single-quotes for string literals inside an MDL expression.)

### Computed dimensions — the bucket-class idiom

A widget has **no computed inline style**: `Style:` is a static string and
`DynamicClasses:` returns *class names*, not CSS values. So anything with a
data-driven dimension — a progress bar width, a bar-chart height, a meter fill —
can't be `Style: 'width: {expr}%'`. The idiom is to **quantise the value into a
bucket and generate one class per bucket**:

1. In a microflow, publish an integer bucket attribute (e.g. `0..20`):
`$obj/PctBucket = round($obj/Done / $obj/Total * 20)`.
2. Generate the classes once with an SCSS `@for` loop:

```scss
@for $i from 0 through 20 { .ss-pb-#{$i} { width: $i * 5%; } }
```

3. Select the class from the bucket:
`DynamicClasses: '''ss-pb-'' + toString($currentObject/PctBucket)'`.

Trade-off worth noting: this adds one bucket attribute per animated dimension to the
domain model. Pick a bucket count that matches the visual precision you need (20 → 5%
steps is usually plenty).

### Adding classes to an existing page

Use `alter page` to attach a class without rewriting the page (see `alter-page.md`):
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/patterns-data-processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ begin
-- Collect items to remove
loop $item in $Items
begin
if not $item/IsActive then
if not($item/IsActive) then
add $item to $ToRemove;
end if;
end loop;
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/mendix/resolve-forward-references.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This applies to the following reference types:
| `snippetcall` | page / snippet | snippet created after the page |
| `show_page` in action | page / snippet | page created after the page that references it |
| `SHOW PAGE` | microflow | page created after the microflow |
| `call microflow` | microflow | already works — microflow resolver checks in-session cache |
| `call microflow` | microflow | callee microflow created after the caller (in the same script, `exec` resolves the call against the project/backend, not later same-script definitions — so order the callee first) |

> **Note:** `SHOW PAGE` inside a microflow body resolves the page reference at
> microflow-creation time, not at invocation time. If the target page doesn't exist yet,
Expand Down
7 changes: 6 additions & 1 deletion .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,9 +818,14 @@ empty -- Null/empty value
[%CurrentDateTime%] -- Current date/time
[%CurrentUser%] -- Current user object
toString($value) -- Convert to string
randomInt($max) -- Random integer
```

> **No `randomInt`.** Mendix has no `randomInt` function. Use `random()` (returns a
> Decimal in [0,1)) and round to an integer range — e.g. a value in 0..8 is
> `round(random() * 8)`. Because `random()` is a Decimal, assigning it (or `div`,
> `secondsBetween()`, and the other `*Between` functions) directly to an `integer`
> variable fails the build with CE0117 — wrap it in `round()`/`floor()`/`ceil()`.

## Complete Example

```mdl
Expand Down
4 changes: 2 additions & 2 deletions docs-site/src/language/associations.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ Controls what happens when an associated object is deleted.
| Behavior | MDL Keyword | Description |
|----------|-------------|-------------|
| Keep references | `DELETE_BUT_KEEP_REFERENCES` | Delete the object, set references to null |
| Cascade | `DELETE_CASCADE` | Delete associated objects as well |
| Cascade | `DELETE_AND_REFERENCES` | Delete associated objects as well |

```sql
/** Invoice must be deleted with Order */
CREATE ASSOCIATION Sales.Order_Invoice
FROM Sales.Order
TO Sales.Invoice
TYPE Reference
DELETE_BEHAVIOR DELETE_CASCADE;
DELETE_BEHAVIOR DELETE_AND_REFERENCES;
```

## Naming Convention
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/language/lexical-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ BOOLEAN, BOTH, BUSINESS, BY, CALL, CANCEL, CAPTION, CASCADE,
CATALOG, CHANGE, CHILD, CLOSE, COLUMN, COMBOBOX, COMMIT, CONNECT,
CONFIGURATION, CONNECTOR, CONSTANT, CONSTRAINT, CONTAINER, CREATE,
CRUD, DATAGRID, DATAVIEW, DATE, DATETIME, DECLARE, DEFAULT, DELETE,
DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_CASCADE, DEMO,
DELETE_BEHAVIOR, DELETE_BUT_KEEP_REFERENCES, DELETE_AND_REFERENCES, DEMO,
DEPTH, DESC, DESCENDING, DESCRIBE, DIFF, DISCONNECT, DROP, ELSE,
EMPTY, END, ENTITY, ENUMERATION, ERROR, EVENT, EVENTS, EXECUTE,
EXEC, EXIT, EXPORT, EXTENDS, EXTERNAL, FALSE, FOLDER, FOOTER,
Expand Down
6 changes: 3 additions & 3 deletions docs-site/src/reference/domain-model/create-association.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
TO to_module.to_entity
TYPE { Reference | ReferenceSet }
[ OWNER { Default | Both | Parent | Child } ]
[ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_CASCADE } ]
[ DELETE_BEHAVIOR { DELETE_BUT_KEEP_REFERENCES | DELETE_AND_REFERENCES } ]

## Description

Expand All @@ -34,7 +34,7 @@ The `DELETE_BEHAVIOR` clause controls what happens when an object on the FROM si
| Behavior | Description |
|----------|-------------|
| `DELETE_BUT_KEEP_REFERENCES` | Delete the object and set references to null |
| `DELETE_CASCADE` | Delete the object and all associated objects on the TO side |
| `DELETE_AND_REFERENCES` | Delete the object and all associated objects on the TO side |

If `OR MODIFY` is specified, the statement is idempotent: if the association already exists, it is updated to match the new definition.

Expand Down Expand Up @@ -94,7 +94,7 @@ CREATE ASSOCIATION Sales.Order_Invoice
FROM Sales.Order
TO Sales.Invoice
TYPE Reference
DELETE_BEHAVIOR DELETE_CASCADE;
DELETE_BEHAVIOR DELETE_AND_REFERENCES;
```

### Idempotent with OR MODIFY
Expand Down
9 changes: 7 additions & 2 deletions scripts/check-skill-mdl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ while IFS= read -r md; do

for stmt in "$WORK"/stmt_*.mdl; do
[ -e "$stmt" ] || continue
first="$(grep -vE '^[[:space:]]*$' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//' | tr 'A-Z' 'a-z')"
# First keyword = first line that is not blank, not a `/** … */` doc
# comment (the `create` statement's Documentation), and not a `--` line
# comment. Without skipping the doc comment, a `/** */`-prefixed DDL
# statement is misclassified as non-DDL and silently skipped — which let
# `index … on (…)` drift ship in mdl-entities.md (findings #4).
first="$(grep -vE '^[[:space:]]*($|/\*|\*|--)' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//' | tr 'A-Z' 'a-z')"
# Only in-scope statements: domain-model DDL, or complete CREATE/DROP of a
# page or snippet.
printf '%s' "$first" | grep -qE "$DDL_RE|$PAGE_RE" || { SKIPPED=$((SKIPPED + 1)); continue; }
Expand All @@ -141,7 +146,7 @@ while IFS= read -r md; do
# enforced here.
if printf '%s' "$out" | grep -qiE 'Syntax errors found|mismatched input|no viable alternative|extraneous input|expecting'; then
FAILED=1
echo "FAIL: $md — $(grep -vE '^[[:space:]]*$' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//')"
echo "FAIL: $md — $(grep -vE '^[[:space:]]*($|/\*|\*|--)' "$stmt" | head -1 | sed -E 's/^[[:space:]]+//')"
printf '%s\n' "$out" | grep -iE 'mismatched|expecting|no viable|extraneous' | grep -iv 'vibe' | sed 's/^/ /' | head -2
fi
done
Expand Down
Loading