From d10c7e9d015afe208b4085ba9463e20b10ab6b54 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:02:38 +0200 Subject: [PATCH 1/3] docs(datagrid-web): add source reorganization proposal (feature-slices + kernel/shell) Co-Authored-By: Claude Opus 4.8 --- .../docs/architecture-reorganization.md | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md diff --git a/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md new file mode 100644 index 0000000000..a85a723c99 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md @@ -0,0 +1,190 @@ +# Datagrid-web — Proposed Source Reorganization + +> **Status:** Proposal / RFC. Nothing here is implemented yet. +> **Scope:** `packages/pluggableWidgets/datagrid-web/src` file/module organization only. +> This does **not** propose rewriting the DI/MobX architecture — that internal design +> (gate → atoms → stores → view-models, wired with `brandi` + `@mendix/widget-plugin-mobx-kit`) +> is sound. The proposal is about **where files live** and **which direction dependencies flow**. + +## 1. Current module tree + +``` +src/ +├── Datagrid.tsx, Datagrid.xml, Datagrid.editorConfig.ts, ... # widget entry + Studio Pro config +├── components/ (43 files, ~2144 LOC) presentational + container React components +│ ├── Grid, GridBody, GridHeader, Header, Row, RowsRenderer, DataCell, ... +│ ├── icons/, loader/ +│ └── __tests__/ +├── features/ (32 files, ~3037 LOC) feature slices (partial) +│ ├── base/ WidgetRoot.viewModel +│ ├── data-export/ controller, request, dialog, hooks +│ ├── row-interaction/ cell/checkbox event controllers + handlers +│ ├── select-all/ container, viewModels, dialogs, hooks +│ ├── selection-counter/, empty-message/, pagination/ +├── model/ (24 files, ~2248 LOC) DI + MobX "backend" +│ ├── containers/ Root / Datagrid / createDatagridContainer +│ ├── models/ rows, columns, grid atoms +│ ├── stores/ GridSize, PageSize ← stores location #1 +│ ├── services/ Datasource, Selection, Texts, Loader, MainGate, Setup +│ ├── hooks/ injection-hooks, useDatagridContainer +│ ├── configs/, tokens.ts +├── helpers/ (13 files, ~1297 LOC) +│ ├── state/ ColumnGroup, ColumnsSorting, GridBasicData, GridPersonalization, column/* ← stores location #2 +│ ├── storage/ personalization storage +│ └── ColumnBase, useDataGridJSActions +├── typings/ (leaf), ui/ (scss), utils/ (columns-hash, test-utils) +``` + +## 2. Critical assessment + +The `model/` layer is genuinely well-designed. The problems are in the **top-level +organization** and in **boundary leakage** between directories. + +### 2.1 Three competing organizing principles at the top level +- `components/` is organized **by technical type** (all React together). +- `features/` is organized **by feature slice**. +- `model/` + `helpers/` are organized **by architectural layer**. + +A reader cannot form one mental model. To understand "selection" you must visit +`features/select-all/`, `features/selection-counter/`, +`model/services/SelectionGate.service.ts`, `components/SelectorCell.tsx` + +`CheckboxCell.tsx`, and `helpers/state/` for sorting. One capability is smeared across +four taxonomies. + +### 2.2 Stateful MobX stores live in (at least) three places +- `model/stores/` — `GridSize.store.ts`, `PageSize.store.ts` (2 stores) +- `helpers/state/` — `ColumnGroupStore`, `ColumnsSortingStore`, `GridBasicData`, + `GridPersonalizationStore`, `column/ColumnStore`, `column/ColumnFilterStore` (6 stores) +- plus observable **view-models** in `features/*` and observable **services** in `model/services/*`. + +There is no principled reason `PageSizeStore` lives in `model/stores/` but +`ColumnsSortingStore` lives in `helpers/state/`. This is the clearest symptom that +**`helpers/` is a junk drawer** that accreted stateful domain code belonging to the model. + +### 2.3 Four directory-level dependency cycles +Confirmed by tracing non-test `import` statements: + +- `components ↔ features` +- `model ↔ features` +- `model ↔ helpers` +- indirect `components → model → features → components` + +The hub is **`model/tokens.ts`**, which imports view-models from `features/` and stores +from `helpers/`, while those same features import back from `model/`. `brandi` DI tokens +**soften** this (coupling is often to token identity, not concrete classes), but the +module graph is still cyclic — no layer can be understood or extracted in isolation. + +### 2.4 Components are service locators, not presentational +Components make ~16 direct imports from `model/` and ~8 from `features/`. Examples: +`Grid.tsx` calls `useGridSizeStore()` / `useGridStyle()`; `ColumnProvider.tsx` imports raw +`CORE_TOKENS`. The "components are dumb" principle is violated — they reach into the DI +container mid-render, which is what makes `components ↔ features` cyclic. + +### 2.5 `features/` is a false promise +It looks like feature-slicing, but only the **late-added** capabilities live there +(data-export, select-all, selection-counter, pagination, empty-message, row-interaction). +The **core** capabilities they'd naturally own (columns, rows, sorting, filtering, the grid +shell) are scattered into `components/`, `helpers/state/`, and `model/`. The codebase is +feature-sliced for new features and layer-sliced for core ones. + +## 3. Proposed reorganization: commit to feature-slices + a thin shared kernel + +Keep the excellent `model/` **internal** pattern, but stop having one monolithic `model/`. +Give every capability its own model + view + wiring, backed by a small shared kernel. +The top level should name **what the widget does**, not **what React/MobX are**. + +``` +src/ +├── Datagrid.tsx, Datagrid.xml, *.editorConfig.ts # entry (unchanged) +│ +├── kernel/ # reusable DI/MobX/gate machinery — feature-agnostic +│ ├── container/ # Root/Datagrid container assembly, createDatagridContainer +│ ├── gate/ # MainGateProvider, gate types (was model/services/MainGate*) +│ ├── tokens.ts # ONLY cross-feature tokens (mainGate, config, setupService) +│ └── react/ # useDatagridContainer, useSetup bindings +│ +├── features/ # EVERY capability is a slice; each owns its full stack +│ ├── data/ # datasource, rows, query params, loader +│ │ ├── rows.model.ts DatasourceService DatasourceParamsController DerivedLoaderController +│ │ ├── data.tokens.ts data.container.ts useData.ts # feature-local wiring +│ │ └── __tests__/ +│ ├── columns/ # ColumnGroupStore, ColumnStore, ColumnProvider, resizer, selector +│ ├── sorting/ # ColumnsSortingStore + header sort UI +│ ├── filtering/ # ColumnFilterStore + filter host wiring +│ ├── selection/ # MERGE select-all + selection-counter + SelectionGate + Selector/CheckboxCell +│ ├── pagination/ # pagination atoms/config/VM + Pagination.tsx + PageSizeStore +│ ├── row-interaction/ # (already good — keep, add its Cell components) +│ ├── data-export/ # (already good) +│ ├── personalization/ # GridPersonalizationStore + storage/* (was helpers/) +│ └── empty-state/ # (was empty-message) +│ +├── shell/ # the grid frame: composition-only, no domain logic +│ ├── Widget.tsx WidgetRoot.tsx WidgetHeader/Footer/TopBar Grid GridBody GridHeader Row +│ └── WidgetRoot.viewModel.ts # (was features/base) +│ +└── shared/ # true leaves: imported-by-many, import-nothing-of-ours + ├── types/ (was typings/) + ├── ui/ icons/, loader/, PseudoModal, scss + └── utils/ columns-hash, test-utils +``` + +### 3.1 Rules that make this hold (and kill the cycles) + +1. **Strict dependency direction:** `shell → features → kernel → shared`. + Features may depend on the kernel and shared, **never on each other or on shell**. + Shell composes features, but no feature imports shell. + Enforce with `eslint-plugin-boundaries` or `import/no-restricted-paths` so cycles + cannot silently return. +2. **Delete `helpers/` entirely.** Every file in it is stateful domain code and moves into + the owning feature (`columns/`, `sorting/`, `personalization/`). "Helpers" and + "utils-with-state" stop existing. +3. **One home for each store.** A store lives in its feature. `PageSizeStore → pagination/`, + `ColumnsSortingStore → sorting/`. No more `model/stores` vs `helpers/state` split. +4. **Split `tokens.ts`.** Today it is the coupling hub because it imports every feature's + view-model. Instead, each feature declares its **own** `*.tokens.ts` and its own + `*.container.ts` binding group. (The container code already has `_01`…`_09` binding + groups — they are begging to be co-located with their features.) `kernel/tokens.ts` + keeps only genuinely cross-feature tokens. **This is the highest-leverage change** — it + removes the `model ↔ features` cycle at its source. +5. **Components stop being service locators.** Feature components read their own feature's + co-located injection-hooks; the `shell/` frame receives feature-rendered subtrees by + composition. `Grid.tsx` in `shell/` should not call `useGridSizeStore` — the pagination + and columns features should hand it what it needs. + +### 3.2 Target dependency direction (acyclic) + +``` +shell ──▶ features ──▶ kernel ──▶ shared + │ │ ▲ + └────────────┴──────────────────────────┘ + (both may import shared) + +features do NOT import each other, and NOTHING imports shell. +``` + +## 4. Migration path (incremental, no big-bang) + +Each step is independently shippable and leaves the widget working. + +| Step | Change | Risk | Value | +|------|--------|------|-------| +| 1 | Rename `typings → shared/types`, `ui → shared/ui`, move `utils → shared/utils`. Pure leaf move. | Very low | Immediate clarity | +| 2 | Dissolve `helpers/`: move each store into a new feature folder (`columns/`, `sorting/`, `personalization/`). Path updates only. | Low | Removes `model ↔ helpers` cycle; single store home | +| 3 | Co-locate DI wiring: move each `_0N_*Bindings` group + its tokens next to its feature; shrink `model/tokens.ts` to the kernel set. | Medium (the real work) | Removes `model ↔ features` cycle | +| 4 | Introduce `kernel/` and `shell/`; move container assembly + frame components. | Medium | Screaming architecture at top level | +| 5 | Add `eslint-plugin-boundaries` rule to lock in layering. | Low | Prevents regression | + +**Reduced-scope option:** if the team is not actively adding features here, doing only +steps 2–3 (kill `helpers/`, split `tokens.ts`) captures roughly 70% of the value for +roughly 30% of the risk. + +## 5. Caveats + +- This is a large refactor of a **mature, shipping widget** with snapshot and E2E tests. + The `model/` internals are good — the proposal **redistributes** them, it does not rewrite + them. Payoff is navigability and testability-in-isolation; cost is a large diff and review + burden. +- Coupling was traced via static `import` analysis, not runtime. Before committing, confirm + no reflection / string-based token lookups would break under file moves. +- Renames touch many import paths; do them mechanically (codemod / IDE move) and lean on the + test suite + `tsc` after each step. From 3df278cc0fcc79ebb025938664b4f9862b89d9e5 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:12:46 +0200 Subject: [PATCH 2/3] docs(datagrid-web): account for @mendix/widget-plugin deps (kernel is external, slices are adapters, 5-tier layering) Co-Authored-By: Claude Opus 4.8 --- .../docs/architecture-reorganization.md | 124 ++++++++++++------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md index a85a723c99..eafe4f2b34 100644 --- a/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md +++ b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization.md @@ -87,33 +87,51 @@ The **core** capabilities they'd naturally own (columns, rows, sorting, filterin shell) are scattered into `components/`, `helpers/state/`, and `model/`. The codebase is feature-sliced for new features and layer-sliced for core ones. -## 3. Proposed reorganization: commit to feature-slices + a thin shared kernel +### 2.6 A large share of the "backend" is NOT local — it lives in `@mendix/widget-plugin-*` +This is the finding that most shapes the proposal. Tracing external imports (non-test): -Keep the excellent `model/` **internal** pattern, but stop having one monolithic `model/`. -Give every capability its own model + view + wiring, backed by a small shared kernel. -The top level should name **what the widget does**, not **what React/MobX are**. +| Package | Import sites | What comes from it | +|---|---|---| +| `@mendix/widget-plugin-grid` | 72 | selection, select-all, selection-counter, pagination, event-switch, keyboard-navigation, ClickActionHelper — **cross-widget domain logic** | +| `@mendix/widget-plugin-mobx-kit` | 29 | `GateProvider`, `SetupHost`, `disposeBatch`, `useConst`, `useSetup` — **the reactive kernel itself** | +| `@mendix/widget-plugin-filtering` | 16 | filter stores/host — filtering domain | +| `@mendix/widget-plugin-platform` / `-component-kit` / `-hooks` / `filter-commons` | ~20 | shared UI + platform helpers | + +**53 of the `src` files import from `@mendix/widget-plugin-*`.** Two consequences: + +- **The reactive kernel is already external.** `widget-plugin-mobx-kit` provides the + gate/setup/dispose primitives. There is nothing local to "extract" as a kernel. +- **Several "features" are adapters, not owners.** `select-all`, `selection-counter`, + `pagination`, and `row-interaction` are thin local wiring over logic that lives in + `widget-plugin-grid`. The datagrid-web slice is a *binding layer*; the brain is in the plugin. + +datagrid-web is best understood as a **composition layer over shared grid-family +packages**, not a self-contained app. The reorganization must reflect that. + +## 3. Proposal: feature-slices over a thin local composition layer + +Keep the excellent DI/MobX **pattern**, but stop having one monolithic `model/`. Give every +capability its own model + view + wiring, and recognise the external plugins as the base of +the stack. The top level should name **what the widget does**, not **what React/MobX are**. ``` src/ ├── Datagrid.tsx, Datagrid.xml, *.editorConfig.ts # entry (unchanged) │ -├── kernel/ # reusable DI/MobX/gate machinery — feature-agnostic +├── composition/ # pure WIRING — no reactive primitives (those are external) │ ├── container/ # Root/Datagrid container assembly, createDatagridContainer -│ ├── gate/ # MainGateProvider, gate types (was model/services/MainGate*) │ ├── tokens.ts # ONLY cross-feature tokens (mainGate, config, setupService) -│ └── react/ # useDatagridContainer, useSetup bindings +│ └── react/ # useDatagridContainer +│ # Gate/Setup/dispose primitives are NOT here — they come from @mendix/widget-plugin-mobx-kit. │ -├── features/ # EVERY capability is a slice; each owns its full stack -│ ├── data/ # datasource, rows, query params, loader -│ │ ├── rows.model.ts DatasourceService DatasourceParamsController DerivedLoaderController -│ │ ├── data.tokens.ts data.container.ts useData.ts # feature-local wiring -│ │ └── __tests__/ +├── features/ # each slice = LOCAL WIRING + widget-specific UI over a plugin +│ ├── data/ # rows.model, DatasourceService, ParamsController, Loader │ ├── columns/ # ColumnGroupStore, ColumnStore, ColumnProvider, resizer, selector │ ├── sorting/ # ColumnsSortingStore + header sort UI -│ ├── filtering/ # ColumnFilterStore + filter host wiring -│ ├── selection/ # MERGE select-all + selection-counter + SelectionGate + Selector/CheckboxCell -│ ├── pagination/ # pagination atoms/config/VM + Pagination.tsx + PageSizeStore -│ ├── row-interaction/ # (already good — keep, add its Cell components) +│ ├── filtering/ # adapter over @mendix/widget-plugin-filtering + ColumnFilterStore +│ ├── selection/ # adapter over widget-plugin-grid/{selection,select-all,selection-counter} +│ ├── pagination/ # adapter over widget-plugin-grid/pagination + PageSizeStore + Pagination.tsx +│ ├── row-interaction/ # adapter over grid/event-switch + keyboard-navigation │ ├── data-export/ # (already good) │ ├── personalization/ # GridPersonalizationStore + storage/* (was helpers/) │ └── empty-state/ # (was empty-message) @@ -122,45 +140,66 @@ src/ │ ├── Widget.tsx WidgetRoot.tsx WidgetHeader/Footer/TopBar Grid GridBody GridHeader Row │ └── WidgetRoot.viewModel.ts # (was features/base) │ -└── shared/ # true leaves: imported-by-many, import-nothing-of-ours +└── shared/ # LOCAL leaves only: imported-by-many, import-nothing-of-ours ├── types/ (was typings/) ├── ui/ icons/, loader/, PseudoModal, scss └── utils/ columns-hash, test-utils ``` -### 3.1 Rules that make this hold (and kill the cycles) +> **Why `composition/` and not `kernel/`?** An earlier draft proposed a local `kernel/`. +> That was redundant: the feature-agnostic reactive kernel already exists as +> `@mendix/widget-plugin-mobx-kit`. What remains local is only *wiring* — container +> assembly and cross-feature tokens — so the layer is named for what it does. + +### 3.1 The five-tier dependency direction (acyclic) + +``` +shell → features → composition → shared (local) → @mendix/widget-plugin-* (external base) + │ │ ▲ + └──────────┴─────────────── may import shared + external ─────────────┘ + +Rules: + • features do NOT import each other, and NOTHING imports shell. + • the external @mendix/widget-plugin-* packages are the allowed-from-anywhere base layer. + • local `shared/` is leaves only — it must not import from features/composition/shell. +``` + +### 3.2 Rules that make this hold (and kill the cycles) -1. **Strict dependency direction:** `shell → features → kernel → shared`. - Features may depend on the kernel and shared, **never on each other or on shell**. - Shell composes features, but no feature imports shell. - Enforce with `eslint-plugin-boundaries` or `import/no-restricted-paths` so cycles - cannot silently return. +1. **Strict dependency direction** as above. Enforce with `eslint-plugin-boundaries` or + `import/no-restricted-paths`. **The lint config must allow-list `@mendix/widget-plugin-*` + as an importable base tier from every layer** — otherwise the rule fires on 53 files of + legitimate external imports. 2. **Delete `helpers/` entirely.** Every file in it is stateful domain code and moves into the owning feature (`columns/`, `sorting/`, `personalization/`). "Helpers" and - "utils-with-state" stop existing. + "utils-with-state" stop existing. *(Unaffected by the plugin finding — pure local win.)* 3. **One home for each store.** A store lives in its feature. `PageSizeStore → pagination/`, `ColumnsSortingStore → sorting/`. No more `model/stores` vs `helpers/state` split. 4. **Split `tokens.ts`.** Today it is the coupling hub because it imports every feature's view-model. Instead, each feature declares its **own** `*.tokens.ts` and its own `*.container.ts` binding group. (The container code already has `_01`…`_09` binding - groups — they are begging to be co-located with their features.) `kernel/tokens.ts` - keeps only genuinely cross-feature tokens. **This is the highest-leverage change** — it - removes the `model ↔ features` cycle at its source. + groups — they are begging to be co-located with their features.) `composition/tokens.ts` + keeps only genuinely cross-feature tokens. **Highest-leverage change** — it removes the + `model ↔ features` cycle at its source. *(The plugin finding reinforces this: most of what + a "kernel" would hold isn't even in this repo, so the central token file should be tiny.)* 5. **Components stop being service locators.** Feature components read their own feature's co-located injection-hooks; the `shell/` frame receives feature-rendered subtrees by - composition. `Grid.tsx` in `shell/` should not call `useGridSizeStore` — the pagination - and columns features should hand it what it needs. + composition. `Grid.tsx` in `shell/` should not call `useGridSizeStore`. -### 3.2 Target dependency direction (acyclic) +### 3.3 Slices are adapters, not owners +Because `widget-plugin-grid` / `-filtering` already own the domain logic for selection, +pagination, keyboard navigation, event handling, and filtering, a **local slice must hold +only wiring + widget-specific UI, and must not re-implement anything the plugin owns.** -``` -shell ──▶ features ──▶ kernel ──▶ shared - │ │ ▲ - └────────────┴──────────────────────────┘ - (both may import shared) +- `selection/` **wraps** `widget-plugin-grid/selection` + `select-all.model` + + `selection-counter` — it does not fork them. +- `pagination/` **wraps** `widget-plugin-grid/pagination` and adds the local `Pagination.tsx` + and `PageSizeStore`. +- `row-interaction/` **wraps** `grid/event-switch` + `keyboard-navigation`. -features do NOT import each other, and NOTHING imports shell. -``` +This guards against the failure mode where "move everything into feature slices" tempts +contributors to duplicate shared grid logic locally. When a slice grows logic that other +grid-family widgets would want, that logic belongs **upstream in the plugin**, not in the slice. ## 4. Migration path (incremental, no big-bang) @@ -170,13 +209,13 @@ Each step is independently shippable and leaves the widget working. |------|--------|------|-------| | 1 | Rename `typings → shared/types`, `ui → shared/ui`, move `utils → shared/utils`. Pure leaf move. | Very low | Immediate clarity | | 2 | Dissolve `helpers/`: move each store into a new feature folder (`columns/`, `sorting/`, `personalization/`). Path updates only. | Low | Removes `model ↔ helpers` cycle; single store home | -| 3 | Co-locate DI wiring: move each `_0N_*Bindings` group + its tokens next to its feature; shrink `model/tokens.ts` to the kernel set. | Medium (the real work) | Removes `model ↔ features` cycle | -| 4 | Introduce `kernel/` and `shell/`; move container assembly + frame components. | Medium | Screaming architecture at top level | -| 5 | Add `eslint-plugin-boundaries` rule to lock in layering. | Low | Prevents regression | +| 3 | Co-locate DI wiring: move each `_0N_*Bindings` group + its tokens next to its feature; shrink the central `tokens.ts` to the cross-feature set. | Medium (the real work) | Removes `model ↔ features` cycle | +| 4 | Introduce `composition/` and `shell/`; move container assembly + frame components. | Medium | Screaming architecture at top level | +| 5 | Add `eslint-plugin-boundaries` rule to lock in the 5-tier layering, **including the `@mendix/widget-plugin-*` allow-listed base tier**. | Low | Prevents regression | **Reduced-scope option:** if the team is not actively adding features here, doing only steps 2–3 (kill `helpers/`, split `tokens.ts`) captures roughly 70% of the value for -roughly 30% of the risk. +roughly 30% of the risk. Both steps are untouched by the external-plugin finding. ## 5. Caveats @@ -188,3 +227,6 @@ roughly 30% of the risk. no reflection / string-based token lookups would break under file moves. - Renames touch many import paths; do them mechanically (codemod / IDE move) and lean on the test suite + `tsc` after each step. +- datagrid-web sits on top of `@mendix/widget-plugin-*` (53 import sites). Treat those + packages as a fixed base layer for this refactor; any change to them is a separate, + cross-widget effort with its own review. From 9f297c60ceb1c2024599bf073bef88ecc58243b6 Mon Sep 17 00:00:00 2001 From: Illia Obukhau <8282906+iobuhov@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:20:48 +0200 Subject: [PATCH 3/3] docs(datagrid-web): add phased migration task list for reorganization Co-Authored-By: Claude Opus 4.8 --- .../architecture-reorganization-tasklist.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization-tasklist.md diff --git a/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization-tasklist.md b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization-tasklist.md new file mode 100644 index 0000000000..6797784d15 --- /dev/null +++ b/packages/pluggableWidgets/datagrid-web/docs/architecture-reorganization-tasklist.md @@ -0,0 +1,159 @@ +# Datagrid-web Reorganization — Migration Task List + +> Companion to [`architecture-reorganization.md`](./architecture-reorganization.md). +> Ordered, incremental, each phase independently shippable. **Do not batch phases into one PR.** +> +> **Global rules for every task** +> - Move files, don't rewrite. Prefer IDE "Move" / a codemod so imports update mechanically. +> - After each task: `pnpm run test` and `pnpm exec tsc --noEmit` must pass; snapshots unchanged +> (or intentionally re-generated with `pnpm run test -u` and reviewed). +> - Do not touch `dist/`, generated files, or `@mendix/widget-plugin-*` package sources. +> - One phase = one PR. Keep diffs move-only where possible so review stays tractable. + +--- + +## Phase 0 — Guardrails first (no moves yet) + +- [ ] **0.1 Snapshot the current graph.** Run the DI graph + a module-import graph and attach + both to the tracking issue, so cycles are visible before/after. + _Verify:_ images generated. _Accept:_ four cycles from §2.3 are visible in the "before". +- [ ] **0.2 Baseline green.** Record current `pnpm run test` + `tsc --noEmit` + E2E status. + _Accept:_ known-good baseline captured; any pre-existing failures noted so they aren't + blamed on the refactor. +- [ ] **0.3 Add a path alias** (e.g. `@dg/*` → `src/*`) in `tsconfig.json` + jest `moduleNameMapper`, + OR confirm relative imports only. Decide the import style ONCE up front. + _Accept:_ a trivial move (one file) resolves under the chosen scheme in both `tsc` and jest. + +--- + +## Phase 1 — `shared/` leaves (very low risk) + +Pure leaf move. `typings/` and `ui/` import nothing of ours; `utils/` imports only helpers+typings. + +- [ ] **1.1** `typings/` → `shared/types/` + (`CellComponent.ts`, `GridColumn.ts`, `personalization-settings.ts`, `sorting.ts`, `static-info.ts`) +- [ ] **1.2** `ui/` → `shared/ui/` (`DatagridPreview.scss`) and move `components/icons/`, `components/loader/`, + `components/PseudoModal.tsx` into `shared/ui/` too (they are presentation primitives, not features). +- [ ] **1.3** `utils/` → `shared/utils/` (`columns-hash.ts`, `test-utils.tsx`). +- [ ] **1.4** Update all import paths (mechanical). + _Verify:_ `pnpm exec tsc --noEmit && pnpm run test`. _Accept:_ `shared/` imports nothing from + `features/ | composition/ | shell/`; only leaves remain. + +--- + +## Phase 2 — Dissolve `helpers/` (low risk, removes `model ↔ helpers` cycle) + +Every file in `helpers/` is stateful domain code or feature UI. Home each in its owning feature. + +- [ ] **2.1** Create `features/columns/` and move: + `helpers/state/ColumnGroupStore.ts`, `helpers/state/column/ColumnStore.tsx`, + `helpers/state/column/BaseColumnInfo.ts`, `helpers/ColumnBase.ts`, `helpers/ColumnPreview.tsx`, + and `helpers/state/GridBasicData.ts`. +- [ ] **2.2** Create `features/sorting/` and move `helpers/state/ColumnsSortingStore.ts`. +- [ ] **2.3** Create `features/filtering/` and move `helpers/state/column/ColumnFilterStore.tsx`. +- [ ] **2.4** Create `features/personalization/` and move `helpers/state/GridPersonalizationStore.ts` + + the whole `helpers/storage/` dir (`AttributePersonalizationStorage.ts`, + `LocalStoragePersonalizationStorage.ts`, `PersonalizationStorage.ts`). +- [ ] **2.5** Move `helpers/useDataGridJSActions.ts` to the feature that owns JS-actions + (likely `features/row-interaction/` — confirm by its imports). +- [ ] **2.6** Delete the now-empty `helpers/` directory. + _Verify:_ `pnpm exec tsc --noEmit && pnpm run test`. _Accept:_ `helpers/` no longer exists; + `git grep -n "helpers/"` in `src` returns nothing; no store lives outside a feature. + +--- + +## Phase 3 — Split `tokens.ts` + co-locate DI wiring (the real work; removes `model ↔ features` cycle) + +The single highest-leverage change. The container already has `_01`…`_09` binding groups +(`model/containers/Datagrid.container.ts`) — relocate each next to its feature. + +- [ ] **3.1** For each feature, create a local `.tokens.ts` holding only that feature's + tokens; move them out of `model/tokens.ts`. Suggested split by binding group: + `_01 core→ data/columns`, `_02 filter→ filtering`, `_03 loader→ data`, + `_04 emptyState→ empty-state`, `_05 personalization→ personalization`, + `_06 pagination→ pagination`, `_07 selection→ selection`, `_08 rowInteraction→ row-interaction`, + `_09 selectAll→ selection`. +- [ ] **3.2** Move each `_0N_*Bindings` group into a `.container.ts` beside its feature, + exporting a `BindingGroup`. `Datagrid.container.ts` now imports and composes the groups + instead of defining them. +- [ ] **3.3** Co-locate the per-feature `injection-hooks.ts` (already exists for empty-message, + select-all, selection-counter) with each feature; move the relevant hooks out of the central + `model/hooks/injection-hooks.ts`. Central file keeps only cross-feature hooks. +- [ ] **3.4** Shrink the central token file to cross-feature tokens only + (`mainGate`, `config`, `setupService`, and other genuinely shared ones). + _Verify:_ `pnpm exec tsc --noEmit && pnpm run test`; re-run the DI graph. + _Accept:_ `model/tokens.ts` (or its successor) no longer imports from `features/`; + the `model ↔ features` cycle is gone in the regenerated graph. + +> ⚠️ **Highest-risk task in the whole plan.** DI resolution + `postInit` activation order +> (see reorg doc §2.3 / earlier review) is order-sensitive. After 3.2, explicitly verify the +> boot sequence: `paramsService` and `gridSizeStore` still eager-created; `selectionHelper` +> not resolved before its `postInit` rebind. Add a container smoke test (Phase 5.2) BEFORE +> merging this phase if one doesn't exist. + +--- + +## Phase 4 — `composition/` + `shell/` (medium risk) + +- [ ] **4.1** Create `composition/` and move `model/containers/*` → `composition/container/`, + `model/hooks/useDatagridContainer.ts` + `useInfiniteControl.tsx` → `composition/react/`, + the shrunk central tokens → `composition/tokens.ts`. + Note: gate/setup primitives stay in `@mendix/widget-plugin-mobx-kit` — do **not** create a + local kernel. `MainGateProvider.service.ts` (the local gate *instance*) → `composition/`. +- [ ] **4.2** Distribute remaining `model/`: `models/rows.model.ts` + `services/Datasource*`, + `DerivedLoaderController` → `features/data/`; `services/SelectionGate` → `features/selection/`; + `services/Texts.service` → `shared/` or `composition/` (cross-feature); `models/columns.model.ts` + → `features/columns/`; `models/grid.model.ts` → `shell/` or `features/columns/` (by usage); + `stores/PageSize.store.ts` → `features/pagination/`; `stores/GridSize.store.ts` → `shell/` or + `features/columns/`; `configs/Datagrid.config.ts` → `composition/`. Delete empty `model/`. +- [ ] **4.3** Create `shell/` and move the frame components from `components/`: + `Widget`, `WidgetRoot`, `WidgetHeader`, `WidgetFooter`, `WidgetTopBar`, `WidgetContent`, + `Grid`, `GridBody`, `GridHeader`, `Header`, `Row`, `RowsRenderer` + `features/base/WidgetRoot.viewModel.ts`. +- [ ] **4.4** Move remaining feature-specific components from `components/` into their slice: + cells (`DataCell`, `CheckboxCell`, `SelectorCell`, `CellElement`, `CheckboxColumnHeader`) → + `columns/` or `selection/` by role; `ColumnResizer`, `ColumnSelector`, `ColumnProvider` → + `columns/`; `Pagination.tsx` → `pagination/`; `ExportAlert` → `data-export/`; + `RefreshStatus` → `data/`; `EmptyPlaceholder` bits → `empty-state/`. Delete empty `components/`. +- [ ] **4.5** Fix components-as-service-locators: `shell/` frame components receive feature subtrees + by composition instead of calling `useGridSizeStore()` / importing raw `CORE_TOKENS`. + _Verify:_ full `pnpm run test` + `tsc --noEmit` + one manual `pnpm start` smoke run. + _Accept:_ top-level dirs are `composition/ features/ shell/ shared/` (+ entry files); no `model/`, + no `components/`, no `helpers/`. + +--- + +## Phase 5 — Lock it in (low risk) + +- [ ] **5.1 Boundary lint.** Add `eslint-plugin-boundaries` (or `import/no-restricted-paths`) + encoding the 5 tiers: `shell → features → composition → shared → @mendix/widget-plugin-*`. + **Allow-list `@mendix/widget-plugin-*` as importable from every layer** (53 legit sites). + Rules: features cannot import features or shell; nothing imports shell; shared is leaves-only. + _Verify:_ `pnpm run lint` passes clean; deliberately add a bad import and confirm it errors. +- [ ] **5.2 Container smoke test.** A test that builds the real container via + `createDatagridContainer` and asserts key tokens resolve + boot order holds (guards the + Phase 3 activation-order risk against future edits). +- [ ] **5.3 Regenerate the graph** and attach the "after" to the tracking issue. + _Accept:_ zero directory-level cycles; `model/tokens.ts` hub is gone. +- [ ] **5.4 CHANGELOG.** No user-facing behavior change → note as internal refactor only per + repo convention (changelogs are user-facing; this may warrant no entry). + +--- + +## Sequencing & scope options + +``` +Phase 0 (guardrails) ─▶ 1 (shared) ─▶ 2 (helpers) ─▶ 3 (tokens) ─▶ 4 (composition/shell) ─▶ 5 (lock-in) + └─────── reduced scope stops here (~70% value) ───────┘ +``` + +- **Full refactor:** all phases, ~5–6 PRs. +- **Reduced scope (recommended if not actively adding features):** Phases 0 → 2 → 3 → 5.1/5.2 only. + Captures ~70% of the value (kills 3 of 4 cycles, single store home, tiny token file) for ~30% + of the risk; skips the large component/frame reshuffle of Phase 4. + +## Risk ranking (do the scary one deliberately) + +1. **Phase 3** — DI/activation order. Highest risk; add the smoke test first. +2. **Phase 4.4/4.5** — largest diff; component moves + un-service-locating. +3. **Phase 2** — mechanical but touches many stores. +4. **Phase 1, 5** — low risk.