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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,54 @@ so it stays clear which part of the repository actually moved.

## [Unreleased]

### Added

#### flex-fields

- **Two new built-in field types, `Matrix` and `Table` — the first *composite* ones, whose
configuration declares whole field definitions inline.** `Matrix` is a repeatable list of
polymorphic blocks (the admin declares named block types up front, each with its own sub-fields);
`Table` is a homogeneous grid over one shared column schema. Both were written and proven in
Dignite.Site's `Dignite.FlexFields.Site` and are ported here **with the wire format unchanged** —
registration keys `Matrix`/`Table`, configuration keys `Matrix.BlockTypes`/`Table.Columns`, and
camelCase `{blockTypeName, values}` / `{values}` value arrays — so fields already stored against
the Site implementation keep working as-is. They ship as built-ins rather than a bolt-on package
because, unlike `FileExplorer` or `CKEditor`, they depend on nothing outside the kernel's own
vocabulary; what made them worth moving is that the two contracts below have to be answerable
without knowing either concrete type.
- `Dignite.Abp.FlexFields.Abstractions` gains `MatrixFieldType`/`TableFieldType` and their
configuration types, plus four kernel contracts they share: **`ICompositeFieldType`**
(`GetInlineFields`, so a host can ask "does this type contain other fields, and which" without
naming a concrete type — an interface rather than an `IsComposite` bool, because every caller
that asks also has to walk those fields), **`INormalizesValue`** (`Normalize`, the canonical wire
shape — deliberately *not* folded into `Validate`, which returns only errors and never the parsed
value, so a value with the wrong key casing would otherwise validate cleanly and then be stored
verbatim and be unreadable to every camelCase reader downstream), **`InlineFieldDefinition`**
(one inline field; carries `Required`, which a `FlexFieldData` cannot), and
**`CompositeFieldNesting`** (`MaxDepth = 3` and the bounded measurement that enforces it — a
configuration is a tree of unbounded depth and every reader of it recurses, so it is capped once
on write instead of guarded in each reader).
- `Dignite.Abp.FlexFields.Web` gains `Views/Shared/FlexFields/Matrix.cshtml` and `Table.cshtml`,
which recurse through the existing `<flex-field-view>` dispatch for each sub-field rather than
re-implementing rendering per type. No `Search/` partials: both types have
`IndexValueType == null` — a list of composite objects has no typed index column to decompose
into — so neither can be marked `Searchable`.
- `@dignite/ng.flex-fields` gains the matching config / control / view components
(`ff-matrix-config|control|view`, `ff-table-config|control|view`), registered in
`BUILT_IN_FIELD_TYPES`, so an existing `provideFlexFields()` call already covers them.
`FieldTypeDefinition` gains an optional **`composite`** flag, which Matrix and Table set and the
config editors use to stop offering composite types once the nesting limit is reached; the
server's `CompositeFieldNesting` remains the authority, that mirror is a courtesy.
- Neither contract is invoked by the kernel — a host calls them, and the demo now shows both:
`ProductAppService` normalizes the value bag before validating and saving, and
`ProductFieldAppService` refuses a too-deeply-nested configuration on create and update.
- **Localization moved with them**: the `FieldType:Matrix`/`FieldType:Table`, `Matrix:*`, `Table:*`
and `Validate:Matrix:*`/`Validate:Table:*` texts now live in the `FlexFields` resource
(`Dignite.Abp.FlexFields.Abstractions`) instead of Site's own `FlexFieldsSite` resource, in all
four shipped cultures (`en`, `ja`, `zh-Hans`, `zh-Hant`). Three general validation keys the
Angular side's shared error-message helper needs came along with them: **`Validate:MinValue`**,
**`Validate:MaxValue`** and **`Validate:MaxLength`**.

## [10.0.0-rc.15] - 2026-09-05

### Fixed
Expand Down
60 changes: 55 additions & 5 deletions flex-fields/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ before changing any contract; it records what was rejected and why.

| Project | Responsibility | Depends on |
|---|---|---|
| `FlexFields.Abstractions` | `IFieldType`/`FieldTypeBase` + the six built-ins, `IFlexFieldData`, `IHasFlexFields`, `FlexFieldValue`, query vocabulary, localization | ABP Core, Localization |
| `FlexFields.Abstractions` | `IFieldType`/`FieldTypeBase` + the eight built-ins, `IFlexFieldData`, `IHasFlexFields`, `FlexFieldValue`, query vocabulary, localization, plus the composite-type contracts (`ICompositeFieldType`, `INormalizesValue`, `InlineFieldDefinition`, `CompositeFieldNesting`) | ABP Core, Localization |
| `FlexFields.Domain.Shared` | `FlexFieldConsts` only | — |
| `FlexFields.Domain` | `IFlexField` (Entity contract), `IFlexFieldProvider<T>` and the other seams, provider-neutral `FlexFieldValidator`/`FlexFieldValueMigrator` | Abstractions, Domain.Shared, ABP DDD |
| `FlexFields.EntityFrameworkCore` | `FlexFieldIndexValue` (relational-only), index/repository base classes, model-creating extensions | Domain |
| `FlexFields.MongoDB` | Embedded values, native path indexes — deliberately **no** pivot-table type | Domain |
| `FlexFields.Web` | `<flex-field-view>`/`<flex-field-search>` TagHelpers + default `.cshtml` per built-in type — SSR counterpart to the Angular library's `<ff-flex-field-view>`/`<ff-flex-field-search>`. No config/control TagHelpers | Abstractions |
| `FlexFields.Installer` | ABP Studio/Suite install entry point, embeds the module's `.abpmdl` | `Volo.Abp.VirtualFileSystem` |

Bolt-on field types (optional, not part of the six above): `FlexFields.FileExplorer` (the field type
Bolt-on field types (optional, not part of the eight above): `FlexFields.FileExplorer` (the field type
itself, references only Abstractions) and `FlexFields.FileExplorer.Web` (its `<flex-field-view>`
rendering — file name/size/MIME type/link, read straight out of the value the Angular picker already
denormalized at pick time; no search partial, since `FileExplorerFieldType.IndexValueType` is `null`).
Expand Down Expand Up @@ -78,10 +78,56 @@ DbContext of its own to run one.
| `Select` | `SelectFieldType` | `select/` |
| `Boolean` | `BooleanFieldType` | `boolean/` |
| `Tree` | `TreeFieldType` | `tree/` |
| `Matrix` | `MatrixFieldType` | `matrix/` |
| `Table` | `TableFieldType` | `table/` |

Renaming any of these again "for consistency" orphans every field already stored under the current
key. `built-in-field-types.spec.ts` asserts all of them for that reason.

## Composite field types (`Matrix`, `Table`)

Two of the eight built-ins are **composite**: their *configuration* declares further whole field
definitions inline, so a field definition is a tree rather than a flat record. Ported in from
Dignite.Site's `Dignite.FlexFields.Site` with the wire format unchanged — the persisted keys are
`Matrix`/`Table` (registration) and `Matrix.BlockTypes`/`Table.Columns` (configuration), and the values
stay camelCase `{blockTypeName, values}` / `{values}` arrays. Same rule as the table above: these are
stored data, not names to tidy.

- **`ICompositeFieldType`** — `GetInlineFields(configuration)`, flattened. An interface rather than an
`IsComposite` bool because every caller that cares also has to walk the nested fields; a bool would
leave each one switching on the concrete type to reach them.
- **`InlineFieldDefinition`** — one inline field: a Matrix block type's sub-field, or a Table column.
Not a `FlexFieldData`, because it carries `Required` — in the kernel proper that flag belongs to a
field's *usage* (`FlexFieldValue.Required`), and an inline field has no usage record to put it in.
- **`INormalizesValue`** — `Normalize(value)`, the canonical wire shape. Separate from `Validate`
because validation answers "is this acceptable" and returns only errors, never the parsed value: a
value with the wrong key casing validates fine and is then stored verbatim, unreadable by every
camelCase reader downstream.
- **`CompositeFieldNesting.MaxDepth = 3`** — a top-level field may be composite and so may its
sub-fields; what *those* declare must be scalar. `ExceedsMaxDepth` carries its own recursion budget,
because it is the first thing to walk an unvetted client configuration.

Both are `IndexValueType == null` (a list of composite objects has no typed index column), so neither
ships a `Views/Shared/FlexFields/Search/` partial and neither can be marked `Searchable`.

**Neither contract is called by the kernel** — a host calls them, and the demo is the worked example:
`ProductAppService` runs `INormalizesValue.Normalize` over the bag before validating and saving;
`ProductFieldAppService` refuses a configuration `CompositeFieldNesting.ExceedsMaxDepth` reports on,
on both create and update.

On the Angular side the two live in `@dignite/ng.flex-fields` at
`angular/projects/flex-fields/src/lib/field-types/matrix/` and `table/`, registered in
`BUILT_IN_FIELD_TYPES` (so `provideFlexFields()` already covers them — no extra provide call), with
selectors `ff-matrix-config|control|view` and `ff-table-config|control|view`. `FieldTypeDefinition`
gained a `composite?: boolean` that Matrix and Table set, which the config editors use to stop offering
composite types at max depth (`MAX_COMPOSITE_NESTING_DEPTH`/`COMPOSITE_NESTING_DEPTH`/`allowsCompositeAt`
in `field-types/composite-nesting.ts`). That mirror is a courtesy; `CompositeFieldNesting` on the server
is the authority. The two also share `InlineFieldDefinition`/`normalizeInlineFieldDefinitions`
(`field-types/inline-field-definition.ts`) — the client-side counterpart of the C# type, plus the
re-casing a stored *configuration* still needs, since only field *values* go through
`INormalizesValue` — and `flexFieldErrorMessage` (`utils/flex-field-error-message.ts`), which is what
the three `Validate:MinValue`/`MaxValue`/`MaxLength` keys were added to the `FlexFields` resource for.

## The seams

The kernel's only information entry point is `IFlexFieldProvider<TEntity>` — a downstream merges its
Expand Down Expand Up @@ -140,14 +186,18 @@ describes, wired to a real feature instead of the test project's throwaway `Test
- **`Services/ProductFieldAppService.cs`** — field CRUD, demonstrating the ordering
`IFlexFieldValueMigrator` documents: rename rewrites every product's bag *before* the definition's
own `Name` changes; delete removes bag values *before* the definition; flipping `Searchable` calls
`IFlexFieldIndexManager.RebuildAsync()`.
`IFlexFieldIndexManager.RebuildAsync()`. Also the enforcement point for
`CompositeFieldNesting.ExceedsMaxDepth`, on create and update alike.
- **`Services/ProductAppService.cs`** — product CRUD plus `SearchAsync`, POST rather than the GET a
`Get*`-prefixed name would default to. ABP's conventional controllers derive the URL from the
*method name* convention, not from an `[HttpPost("...")]` attribute's route template string — a
method still named `GetListAsync` collides on the same URL as `CreateAsync` no matter what
attribute you add. The rename is why it's `SearchAsync`, at `POST /api/app/product/search`.
attribute you add. The rename is why it's `SearchAsync`, at `POST /api/app/product/search`. Also
where `INormalizesValue.Normalize` runs over the bag, before validating and saving.
- **`Data/ProductDemoDataSeedContributor.cs`** — seeds one `ProductField` per built-in field type
plus the FileExplorer bolt-on, and five products, so a first `dotnet run -- --migrate-database`
(including `Table` and `Matrix`, whose values two of the products carry for real) plus the
FileExplorer bolt-on and two CKEditor ones — eleven fields — and five products, so a first
`dotnet run -- --migrate-database`
leaves the demo immediately browsable instead of empty. One product's `images` field gets a real
uploaded file (`FileDescriptorManager.CreateAsync` directly, bypassing the `[Authorize]`-gated app
service the same way the field/product repositories are used directly elsewhere in this class) into
Expand Down
4 changes: 2 additions & 2 deletions flex-fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ defines "the concrete one."
| Package | Purpose |
|---|---|
| `Dignite.Abp.FlexFields.Domain.Shared` | Shared constants (`FlexFieldConsts`). Dependency-free. |
| `Dignite.Abp.FlexFields.Abstractions` | DDD-free contracts and vocabulary: `IFlexFieldData`/`FlexFieldData`, `IHasFlexFields`/`FlexFieldDictionary`, `FlexFieldValue`, `IFieldType` + the built-in field types (Text/Number/DateTime/Select/Boolean/Tree), the query vocabulary, and the field-lifecycle Etos (`FlexFieldRenamedEto`, `FlexFieldDeletedEto`). Referencing this package alone is enough to implement a custom field type or type a downstream's DTOs. |
| `Dignite.Abp.FlexFields.Abstractions` | DDD-free contracts and vocabulary: `IFlexFieldData`/`FlexFieldData`, `IHasFlexFields`/`FlexFieldDictionary`, `FlexFieldValue`, `IFieldType` + the built-in field types (Text/Number/DateTime/Select/Boolean/Tree, plus the composite Matrix/Table and their `ICompositeFieldType`/`INormalizesValue`/`InlineFieldDefinition`/`CompositeFieldNesting` contracts), the query vocabulary, and the field-lifecycle Etos (`FlexFieldRenamedEto`, `FlexFieldDeletedEto`). Referencing this package alone is enough to implement a custom field type or type a downstream's DTOs. |
| `Dignite.Abp.FlexFields.Domain` | The Entity contract (`IFlexField : IAggregateRoot<Guid>`) and the DDD-aware seams: `IFlexFieldProvider<TEntity>`, `IFlexFieldValidator<TEntity>` (+ default impl), `IFlexFieldIndexManager<TEntity>`, `IFlexFieldQueryExecutor<TEntity>`, `IFlexFieldValueMigrator<TEntity>` (+ its one provider-agnostic default impl), `IFlexFieldRepository<TField>`. |
| `Dignite.Abp.FlexFields.EntityFrameworkCore` | EF Core support (not ownership): `ConfigureFlexFieldsProperty`/`ConfigureFlexField`/`ConfigureFlexFieldIndex` model-builder extensions, the typed pivot-row shape (`FlexFieldIndexValue`), and abstract base classes for the index manager, query executor, and field repository. Ships no `DbContext` and no table of its own. |
| `Dignite.Abp.FlexFields.MongoDB` | MongoDB support: queries and indexes the `FlexFieldDictionary` in place, so writes need almost no index synchronization. Deliberately has **no** counterpart to `FlexFieldIndexValue` — that shape is a relational pivot row. |
| `@dignite/ng.flex-fields` (npm) | Angular UI: config / control / view / search components for all six field types, the `FieldTypeResolver` registry, and `provideFlexFields()`. See [`angular/projects/flex-fields`](./angular/projects/flex-fields/README.md). |
| `@dignite/ng.flex-fields` (npm) | Angular UI: config / control / view / search components for all eight field types, the `FieldTypeResolver` registry, and `provideFlexFields()`. See [`angular/projects/flex-fields`](./angular/projects/flex-fields/README.md). |

## Install

Expand Down
11 changes: 10 additions & 1 deletion flex-fields/angular/projects/flex-fields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ inside `<21.1.0` if you need a single copy; otherwise expect `abp-tree` to run o

## Field types

Six built-in types, each with up to four role components — **config** (design the field),
Eight built-in types, each with up to four role components — **config** (design the field),
**control** (edit a value), **view** (display a value) and **search** (filter by it):

| Registration key | Type | Roles |
Expand All @@ -47,6 +47,15 @@ Six built-in types, each with up to four role components — **config** (design
| `Select` | single or multiple choice | config, control, view, search |
| `Boolean` | boolean | config, control, view, search |
| `Tree` | single or multiple selection from a node tree | config, control, view, search |
| `Matrix` | repeatable list of polymorphic blocks, each block type with its own sub-fields | config, control, view |
| `Table` | repeatable grid, one shared column schema for every row | config, control, view |

`Matrix` and `Table` are **composite**: their configuration declares further fields, and their config,
control and view components recurse through `<ff-flex-field-config>` / `<ff-flex-field-control>` /
`<ff-flex-field-view>` to render them. They ship no search component — the server's
`IndexValueType` is `null` for both, so there is nothing to filter on. How deep the recursion may go
is the server's `CompositeFieldNesting.MaxDepth`; the config editors mirror it only so they can stop
offering composite types once the limit is reached.

The registration keys are the values persisted in `IFlexFieldData.FieldTypeName` on the server.
They are **data, not class names** — `Text` is served by `TextFieldType` in C# and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { FieldTypeResolver } from '../field-types';
import { FieldTypeResolver } from '../field-types/field-type-resolver.service';
import { FlexFieldData } from '../models';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input, OnChanges, Type, ViewChild, ViewContainerRef, inject } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldTypeResolver } from '../field-types';
import { FieldTypeResolver } from '../field-types/field-type-resolver.service';
import { FlexFieldValue } from '../models';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, Input, OnChanges, Type, ViewChild, ViewContainerRef, inject } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldTypeResolver } from '../field-types';
import { FieldTypeResolver } from '../field-types/field-type-resolver.service';
import { FlexFieldValue } from '../models';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, Input, OnChanges, Type, ViewChild, ViewContainerRef, inject } from '@angular/core';
import { FieldTypeResolver } from '../field-types';
import { FieldTypeResolver } from '../field-types/field-type-resolver.service';
import { FlexFieldValue } from '../models';

/** Renders the read-only **display** of one flex field's value, whichever type it is. */
Expand Down
Loading
Loading