Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9775527
feat(test): implement @cleanup rollback
claude Aug 7, 2026
00a6f51
fix(test): run the app's own after-startup microflow during --local
claude Aug 8, 2026
cdc90ee
feat(check): reject validation rules on non-persistent entities (MDL054)
claude Aug 8, 2026
6973a99
fix(exec): don't tell a statement to move before itself
claude Aug 8, 2026
b6cf2a7
Merge branch 'main' into claude/mxcli-unit-test-perf-n7ggx8
claude Aug 8, 2026
cc471f2
feat(check): reject XPath association traversal from a variable (MDL055)
claude Aug 8, 2026
b2c4f20
fix(test): resolve a project-relative path for --list too
claude Aug 8, 2026
bab4d42
fix(odata): publish Integer as Int64, and say when an enum is a string
claude Aug 8, 2026
29481bc
fix(external-entities): read an attribute's OData mapping back
claude Aug 8, 2026
3198948
fix(exec): refuse the XPath constraints check already rejects (#833)
claude Aug 8, 2026
b9827f1
fix(external-entities): honour the contract's capability annotations
claude Aug 8, 2026
df1e52b
fix(odata): use the client's own credentials to fetch $metadata
claude Aug 8, 2026
a32fde9
fix(microflows): pass a dynamic query expression through unquoted
claude Aug 8, 2026
63f72e0
fix(check): advertise the three OData properties MDL-ODATA01 forgot
claude Aug 8, 2026
9377fbb
feat(test): say which after-startup microflow a local run displaces
claude Aug 8, 2026
6b6932e
fix(check): retire the false-positive MDL009, add MDL056 for (empty)
claude Aug 8, 2026
7da49c0
fix(odata): apply published-entity changes on modify
claude Aug 8, 2026
98ddb29
feat(security): CREATE OR MODIFY MODULE ROLE
claude Aug 8, 2026
3e60080
docs(fix-issue): six symptom rows from the formula1 OData batch
claude Aug 8, 2026
ec5d388
Merge pull request #113 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Aug 8, 2026
a5cf260
fix(microflows): write the InheritanceSplit and its case values
claude Aug 8, 2026
4291f1d
fix(external-entities): stop renaming an attribute called `name`
claude Aug 8, 2026
30327d7
feat(move): MOVE JAVA ACTION and MOVE ODATA SERVICE
claude Aug 8, 2026
50cccd9
docs(fix-issue): two symptom rows from the formula1 folders/naming batch
claude Aug 8, 2026
1ec638c
docs(microflows): fix the type-split examples that fail CE0090
claude Aug 8, 2026
6ae5c3d
Merge remote-tracking branch 'origin/main' into claude/mxcli-issues-o…
claude Aug 8, 2026
f64115e
fix(theme): add the widget-module layer, so Data Grid 2 follows the p…
claude Aug 8, 2026
d10c878
Merge pull request #116 from ako/claude/mendix-default-styling-1fgvpm
ako Aug 8, 2026
ecc901a
Merge pull request #115 from ako/claude/mxcli-issues-ovfoxk
ako Aug 8, 2026
52f12d3
Merge remote-tracking branch 'origin/main' into claude/mxbuild-diagno…
claude Aug 8, 2026
c76d4b7
Merge pull request #114 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 8, 2026
8142222
fix(describe): stop inventing an `else` on a type split
claude Aug 8, 2026
ef29b18
Merge branch 'mendixlabs:main' into main
ako Aug 8, 2026
e62b3da
fix(odata): resolve constant credentials for the $metadata fetch
claude Aug 8, 2026
9eed180
fix(alter-page): reach widgets inside a customContent column
claude Aug 8, 2026
7aaa0de
fix(alter-page): set an action button's caption via CaptionTemplate
claude Aug 8, 2026
734b1d6
fix(theme): re-point the filter-operator popovers at the palette
claude Aug 8, 2026
3c1dc5b
docs(fix-issue): two symptom rows from the constant-credential and po…
claude Aug 8, 2026
b48a50c
Add LIST FOLDERS: read a module's folder layout back out of the model
claude Aug 8, 2026
b18eb44
Merge remote-tracking branch 'origin/main' into claude/mxcli-issues-o…
claude Aug 8, 2026
26dce46
Merge pull request #117 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 8, 2026
d36b29e
Merge pull request #118 from ako/claude/mxcli-issues-ovfoxk
ako Aug 8, 2026
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
25 changes: 25 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .claude/skills/mendix/manage-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ create module role MyModule.Admin description 'Full administrative access';
create module role MyModule.User;
create module role MyModule.Viewer description 'Read-only access';

-- `or modify` updates an existing role's description instead of failing, so the
-- whole security script stays re-runnable rather than needing a run-once file.
create or modify module role MyModule.ApiUser description 'API consumer';

-- Remove a module role
drop module role MyModule.Viewer;
```
Expand Down
51 changes: 51 additions & 0 deletions .claude/skills/mendix/organize-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,47 @@ begin
end;
```

## Reading the Layout Back

`list folders` shows the folder layout of a module and what is in each folder.
This is the counterpart to `move`: `move` puts a document somewhere, `list
folders` shows where everything actually is.

```sql
-- One module
list folders in MyModule;

-- Every module in the project
list folders;
```

```
MyModule
(module root) [1]
Microflow ACT_Unfiled
Api [0]
Api/Published [1]
ODataService PublicApi
Support [1]
JavaAction Helper

(3 folder(s), 3 document(s))
```

Three things about the output are deliberate:

- **Empty folders are listed** (`Api [0]`), so the listing is the whole layout
and can be diffed against an intended one.
- **Documents still at the module root** appear under `(module root)` — what is
not filed yet is the thing you most want to notice.
- **Ordering is stable**, so a diff between two runs shows only real movement.

Use the CLI's `--json` flag for a row per document (`Module, Folder, Kind, Document`)
when comparing against a checked-in layout.

Do **not** reach for `show structure` here: it groups by document type at every
depth and never shows which folder a document sits in.

## Moving Documents

The `move` command relocates existing documents between folders and modules.
Expand Down Expand Up @@ -168,8 +209,17 @@ move page OldModule.CustomerPage to NewModule;
| Nanoflow | `folder 'path'` (keyword) | `move nanoflow ...` |
| Snippet | `folder: 'path'` (property) | `move snippet ...` |
| Enumeration | N/A | `move enumeration ...` |
| Constant | N/A | `move constant ...` |
| Database connection | N/A | `move database connection ...` |
| Java action | N/A | `move java action ...` |
| OData service (published) | N/A | `move odata service ...` |
| Entity | N/A | `move entity ...` (module only, no folders) |

**Java actions and published OData services have no folder clause on `create`**, so
`move` is the only way to place them — before this they were stuck at the module
root forever. Both are plain document units, so the move is model-level only: it
changes containment and nothing else.

**Note:** Pages and snippets use property syntax (`folder: 'path'` inside parentheses). Microflows and nanoflows use keyword syntax (`folder 'path'` before `begin`). Entities are embedded in domain models and can only be moved to a different module (no folder support).

## Example: Reorganize a Module
Expand Down Expand Up @@ -243,3 +293,4 @@ drop folder 'Processing' in MyModule;
- [ ] Cross-module moves: checked impact with `show impact of` first
- [ ] Folder naming is consistent across modules
- [ ] DROP FOLDER: verify folder is empty before dropping
- [ ] After a batch of moves: `list folders in MyModule` to confirm the layout
75 changes: 73 additions & 2 deletions .claude/skills/mendix/test-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,48 @@ The markdown format turns your tests into living documentation.
| `@throws` | Expect error | `@throws 'validation failed'` |
| `@cleanup` | Rollback strategy | `@cleanup rollback` (default) or `@cleanup none` |

### `@cleanup` — what happens to a test's data

**`rollback` is the default**, so by default a test's database writes do not
survive it. The endpoint opens a transaction around the call and rolls it back
afterwards, including when the test throws.

```mdl
/**
* @test creating an order does not leak
* @expect $result = 'ok'
*/
$result = CALL MICROFLOW Sales.CreateOrder(Amount = 100);
/

/**
* @test seed data the next test needs
* @cleanup none
*/
$result = CALL MICROFLOW Sales.SeedCatalogue();
/
```

Use `@cleanup none` when the writes are the point — seeding a fixture, or
inspecting the result in the running app afterwards.

Two things worth knowing:

- **`--local` only.** Rollback needs the test endpoint, which owns the context
the test runs in. The Docker / `--legacy-runner` path executes tests inside
the after-startup action and has no such seam, so it always commits.
- **A rollback that fails is reported, loudly.** The run prints a `WARNING` per
affected test and a summary line, because the alternative — data left behind
while the suite still says PASS — is the failure mode this annotation exists
to prevent. `--verbose` tags every test with `[rolled back]`, `[committed]` or
`[ROLLBACK FAILED]`.

A misspelled strategy (`@cleanup rollbak`) is a **parse error**, not a silent
fallback to committing.

Rollback matters most under `--attach`, where the database is the one your dev
app is using.

---

## Running Tests
Expand Down Expand Up @@ -130,8 +172,9 @@ older **after-startup microflow** pattern.
2. Records the project's current after-startup microflow, and whether an `MxTest`
module already exists
3. Generates **one `MxTest.Test_<id>` microflow per test**, plus a Java action
that registers an HTTP endpoint, and points after-startup at a microflow whose
only job is to call it — **no test runs during startup**
that registers an HTTP endpoint, and points after-startup at a microflow that
registers it and then **chains your own after-startup microflow** —
**no test runs during startup**
4. Builds and boots the app once
5. Invokes each test by name over HTTP; each returns its own verdict in the
response
Expand All @@ -150,6 +193,34 @@ Two consequences worth knowing when reading a failing run:
Each test is a separate microflow with its own variable scope, so `$result` in
one test never collides with `$result` in another.

#### Your app's after-startup microflow still runs

The generated startup flow registers the endpoint and then calls the project's
own after-startup microflow, so tests see the app in the state it actually boots
into — a loaded cache, seeded reference data, whatever your app does. The run
says which happened:

```
After-startup set to MxTest.RegisterEndpoint (registers the endpoint; runs no tests, then runs your MyModule.ASU_Startup)
```

Pass `--skip-app-startup` when you want an empty, deterministic baseline
instead — the app seeds demo data and your tests assert on counts, say:

```
After-startup set to MxTest.RegisterEndpoint (… --skip-app-startup, so MyModule.ASU_Startup will NOT run)
```

This is why a suite behaves the same under `--local` and `--attach`. Before it
chained, `--local` ran with the app's startup logic suppressed, and a suite that
depended on startup state passed under `--attach` and failed under `--local` for
reasons unrelated to the code.

One thing rollback does **not** cover: whatever the startup microflow writes
happens at boot, outside any test's transaction, so `@cleanup rollback` does not
undo it. Under `--local` that lands in the scratch `<project>_test` database;
under `--attach` your app wrote it at its own boot regardless.

#### `--watch`: keep the runtime warm

```bash
Expand Down
37 changes: 37 additions & 0 deletions .claude/skills/mendix/theme-styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,43 @@ Keep the rail dark in both variants, or force `color: inherit` on those widgets.
For a working implementation of all of the above, read the generated
`theme/web/_mxcli-atlas-map.scss` in any themed project.

### Tokens stop at Atlas Core — the widget modules bake their colours

Re-pointing Atlas's custom properties covers the app, and then a few things stay
stubbornly off-palette: the Data Grid 2 pager caption, row-select checkboxes,
popover shadows. One cause: the theme source shipped by the **widget modules**
(`themesource/datawidgets`, `atlas_web_content`) styles some things with Sass
variables and literals. Sass resolves those at compile time, before any custom
property exists, so the value is baked into `theme.compiled.css` and **no token
can move it**. Only a later CSS rule can.

The worst case is `datawidgets/web/variables.scss:18`,
`$pagination-caption-color: #0a1325` — the "1–15 of 77" caption, which measured
**1.02:1** on a dark ground. The pager *buttons* beside it were fine, because
they resolve `var(--gray-darker, …)` through Atlas. Same bar, two mechanisms.

**The obvious fix does not work.** Each module's `main.scss` imports
`theme/web/custom-variables` *before* its own `!default` variables, so setting
`$pagination-caption-color: var(--my-muted)` there would win and Sass would
substitute the `var()` into every use site. Tempting, and wrong here:

1. The names collide with Atlas Core's, and Atlas Core feeds them to Sass colour
functions — `atlas_core/web/_variables.scss:20` computes
`mix($brand-primary, #e7e7e9, 10%)`. Handing `mix()` a `var()` is a compile
error, so the app stops building.
2. The worst offenders are not behind a variable at all:
`_three-state-checkbox.scss` writes `#264ae5` and `rgba(#264ae5, 0.4)`
directly, so overriding `$brand-primary` would not reach them.

So it is a rule set, in a partial imported after the theme's own — see the
generated `theme/web/_mxcli-widgets.scss`.

**Read the compiled CSS, not the SCSS, when building one.** The sources are full
of `var(--token, #fallback)` declarations that already resolve correctly; only
the bare literals are a problem. In one measured app the stock blue `#264ae5`
appeared in 46 declarations — **24 of them harmless fallbacks**. Grepping the
source would have produced twice the rules for no benefit.

## CSS Hot-Reload Workflow

For theme/styling changes during Docker development:
Expand Down
44 changes: 38 additions & 6 deletions .claude/skills/mendix/write-microflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,31 +421,63 @@ Use `case` when a microflow branches on an enumeration value.
case $Status
when Open, Pending then
return true;
when (empty) then
when Closed then
return false;
else
when (empty) then
return false;
end case;
```

`(empty)` represents an unset enumeration value. Multiple values can share one `when` branch by separating them with commas. Case values are bare identifiers — do **not** quote them.

> **Every value needs a branch, including `(empty)` — and there is no `else`.**
> A Mendix enum split is an exclusive split with one outgoing flow per condition
> value, so an uncovered value fails the build with **CE0079** *"The 'X' condition
> value should be configured in properties for an outgoing flow."* `mxcli check`
> reports a missing `(empty)` branch as **MDL056**, and an `else` branch as
> **MDL008** (an `else` does not stand in for the missing flows: mxbuild reports
> CE0079 for each uncovered value *and* CE0773 on the else flow itself).
>
> The `(empty)` branch is required **even when the attribute is `not null`** —
> verified on Mendix 11.6.6. If several values share a path, put them in one
> branch (`when Open, Pending then`) rather than reaching for `else`.

### Type Split And Cast Statements

Use `split type` when a microflow branches on an object's runtime specialization.
Use `cast` inside a type branch to create the specialized variable used by the branch body.

```mdl
declare $IsSpecialized boolean = false;
split type $Input
case Sample.SpecializedInput
cast $SpecificInput;
return true;
else
return false;
set $IsSpecialized = true;
case Sample.BaseInput
end split;
return $IsSpecialized;
```

`case` values are qualified entity names. The optional `else` branch handles objects that do not match any listed specialization.
`case` values are qualified entity names.

> **Every type needs a branch — including the base entity.** An object-type
> decision gets one outgoing flow per listed type, and a type with no flow fails
> the build with **CE0090** *"The 'X' value should be configured for an outgoing
> flow."* The base entity (the split variable's own type) counts: `case
> Sample.BaseInput` above is what covers "it is not any of the specializations".
>
> **`else` does not stand in for the base-type case.** It is accepted — it
> serializes as `Microflows$NoCase` — but it does not satisfy coverage, so
> `case Spec` + `else` still fails CE0090. Once every type has a branch, `else`
> is redundant. Verified on Mendix 11.6.6 and 11.13.0.
>
> **The split needs somewhere to go afterwards.** Branch bodies converge on a
> merge that continues to the microflow's end event, so a non-void microflow
> needs a `return` after `end split;` — otherwise `mxcli check` reports MDL003
> and the build fails **CE0067** *"The 'Return value' property is required."*
> Doing the per-branch work into a variable and returning it once (above) is the
> clearest shape; returning inside every branch also works, but still needs the
> trailing `return`.

**`cast` only stores the output variable.** Studio Pro persists Microflows$CastAction with a single `VariableName` field — the source variable is implicit (the type-split's input). Use `cast $SpecificName;` to give the specialized variable its name. The two-variable form `$Output = cast $Source;` parses but `$Source` is dropped on roundtrip; prefer the single-variable form.

Expand Down
Loading
Loading