From 009332adbf3ebf2c2bfddacc2eed6ee430a5c78b Mon Sep 17 00:00:00 2001
From: Babayaga
Date: Fri, 3 Apr 2026 17:15:16 +0200
Subject: [PATCH] types:vfs/cats/groups/posts/pages
---
packages/ui/docs/types.md | 238 ++++++------
.../components/admin/ProductTypeDataForms.tsx | 108 ++++++
.../src/components/admin/ProductsManager.tsx | 249 +++++++++++--
.../src/modules/ecommerce/client-products.ts | 4 +
.../pages/editor/UserPageTypeFields.tsx | 5 +-
.../ui/src/modules/types/RJSFTemplates.tsx | 49 ++-
packages/ui/src/modules/types/TypeBuilder.tsx | 19 +-
.../ui/src/modules/types/TypeCategoryTree.tsx | 64 +++-
.../ui/src/modules/types/TypeRenderer.tsx | 5 +-
.../ui/src/modules/types/appPickerWidgets.tsx | 338 ++++++++++++++++++
.../types/builder/TypeBuilderContent.tsx | 173 ++++++++-
.../types/builder/appPickerWidgetOptions.ts | 48 +++
.../src/modules/types/builder/components.tsx | 2 +-
.../types/builder/typeBuilderCollision.ts | 49 +++
.../ui/src/modules/types/builder/utils.ts | 7 +-
.../modules/types/categoryPickerWidget.tsx | 39 ++
.../src/modules/types/groupPickerWidget.tsx | 39 ++
.../src/modules/types/rjsfButtonTemplates.tsx | 130 +++++++
.../src/modules/types/rjsfWidgetRegistry.ts | 20 ++
packages/ui/src/modules/types/schema-utils.ts | 16 +-
.../ui/src/modules/types/userPickerWidget.tsx | 38 ++
21 files changed, 1476 insertions(+), 164 deletions(-)
create mode 100644 packages/ui/src/components/admin/ProductTypeDataForms.tsx
create mode 100644 packages/ui/src/modules/types/appPickerWidgets.tsx
create mode 100644 packages/ui/src/modules/types/builder/appPickerWidgetOptions.ts
create mode 100644 packages/ui/src/modules/types/builder/typeBuilderCollision.ts
create mode 100644 packages/ui/src/modules/types/categoryPickerWidget.tsx
create mode 100644 packages/ui/src/modules/types/groupPickerWidget.tsx
create mode 100644 packages/ui/src/modules/types/rjsfButtonTemplates.tsx
create mode 100644 packages/ui/src/modules/types/rjsfWidgetRegistry.ts
create mode 100644 packages/ui/src/modules/types/userPickerWidget.tsx
diff --git a/packages/ui/docs/types.md b/packages/ui/docs/types.md
index 163a1a0f..4986d509 100644
--- a/packages/ui/docs/types.md
+++ b/packages/ui/docs/types.md
@@ -1,148 +1,178 @@
# Unified Type System
-The Type System provides a flexible, schema-driven way to define data structures, enums, flags, and relationships within the Polymech platform. It is built on top of Supabase (PostgreSQL) and supports inheritance, validation (JSON Schema), and strict typing.
+Schema-driven definitions for structures, enums, flags, and field wrappers. Persisted in PostgreSQL (Supabase), exposed under `/api/types`, and edited in the app via **visual builder** (`TypeBuilder`) or **JSON schema / UI schema** (`TypeRenderer`). Runtime forms use **@rjsf/core** with templates in `src/modules/types/RJSFTemplates.tsx`.
-## Database Schema
+For a deeper architecture walkthrough (diagrams, sequence sketches), see [`type-system.md`](./type-system.md).
-The system consists of the following core tables in the `public` schema.
+---
-### Core Tables
+## Type kinds (`types.kind`)
-#### `types`
-The main table storing type definitions.
+| Kind | Role |
+|------|------|
+| `primitive` | Built-in value kinds: `string`, `int`, `float`, `bool`, `array`, `object`, `enum`, `flags`, `reference`, `alias`, … |
+| `enum` | Custom enum: values live in `type_enum_values`. |
+| `flags` | Custom flags: values live in `type_flag_values`. |
+| `structure` | Object shape: members listed in `type_structure_fields`. |
+| `field` | **Per-structure field row**: name `{StructureName}.{fieldName}`, `parent_type_id` → the **value** type (primitive, custom enum, flags, nested structure, array target, …). Referenced by `type_structure_fields.field_type_id`. |
+| `alias` | Treated as string in generated JSON Schema until fully resolved. |
-- `id`: UUID (Primary Key)
-- `name`: Text (Unique constraint usually desired but not strictly enforced at DB level yet)
-- `kind`: Enum (`primitive`, `enum`, `flags`, `structure`, `alias`)
-- `parent_type_id`: UUID (Foreign Key -> `types.id`). Supports inheritance.
-- `description`: Text (Optional)
-- `json_schema`: JSONB (JSON Schema fragment validation)
-- `owner_id`: UUID (Foreign Key -> `auth.users.id`)
-- `visibility`: Enum (`public`, `private`, `custom`)
-- `meta`: JSONB (Arbitrary metadata)
-- `settings`: JSONB (UI/Editor settings)
-- `created_at`, `updated_at`: Timestamps
+Structures never point `field_type_id` at a bare custom enum id; they point at a **`field` row** whose parent is that enum. That allows per-field `meta`, `settings` (defaults, `items_type_id`, `group`), and descriptions.
-#### `type_enum_values`
-Values for `enum` types.
+---
-- `id`: UUID
-- `type_id`: UUID (FK -> `types.id`)
-- `value`: Text (The raw value)
-- `label`: Text (Display label)
-- `order`: Integer (Sort order)
+## Database (summary)
-#### `type_flag_values`
-Bit definitions for `flags` types.
+### `types`
-- `id`: UUID
-- `type_id`: UUID (FK -> `types.id`)
-- `name`: Text (Flag name)
-- `bit`: Integer (Power of 2 or bit index)
+- `id`, `name`, `kind`, `parent_type_id` (inheritance / alias target / **field → value type**)
+- `description`, `json_schema` (JSONB), `owner_id`, `visibility` (`public` \| `private` \| `custom`)
+- `meta` (JSONB) — e.g. `meta.uiSchema` for RJSF overrides on this type
+- `settings` (JSONB) — e.g. `default_value`, `items_type_id`, `group` on **field** kinds
+- `created_at`, `updated_at`
-#### `type_structure_fields`
-Field definitions for `structure` types.
+### `type_enum_values` / `type_flag_values`
-- `id`: UUID
-- `structure_type_id`: UUID (FK -> `types.id`)
-- `field_name`: Text
-- `field_type_id`: UUID (FK -> `types.id`)
-- `required`: Boolean
-- `default_value`: JSONB
-- `order`: Integer
+- Enum: `type_id`, `value`, `label`, `order`
+- Flags: `type_id`, `name`, `bit`
-#### `type_casts`
-Transformation rules between types.
+### `type_structure_fields`
-- `from_type_id`: UUID
-- `to_type_id`: UUID
-- `cast_kind`: Enum (`implicit`, `explicit`, `lossy`)
-- `description`: Text
+- `structure_type_id` → structure `types.id`
+- `field_name`, `field_type_id` → **`kind: field`** type id (not the raw enum id)
+- `required`, `default_value` (JSONB), `order`
-## Access Control (RLS)
+`required` is also reflected in generated JSON Schema `required[]`. If you edit the JSON Schema tab and save, the client can sync `structure_fields[].required` from `json_schema.required` when that array is present.
-Row Level Security is enabled on all tables.
+### `type_casts`
-### Policies
-- **Read**:
- - `public` types are visible to **all users** (authenticated and anonymous).
- - `private` and `custom` types are visible only to their **owner** (creator) and **admins**.
-- **Write** (Create, Update, Delete):
- - **Owners** can modify their own types.
- - **Admins** can modify any type.
- - Ordinary users cannot modify system types (types with no owner or owned by system).
+Transformation metadata between types (`from_type_id`, `to_type_id`, `cast_kind`, …).
-## API Endpoints
+---
-The type system is exposed via a RESTful API under `/api/types`.
+## Server API (`/api/types`)
-### List Types
-`GET /api/types`
+Implemented in `server/src/products/serving/db/db-types.ts`. OpenAPI route definitions and Zod shapes live in that file (`TypeWithRelationsSchema`, `ExtendedTypeInsertSchema`, `ExtendedTypeUpdateSchema`).
-**Query Parameters:**
-- `kind`: Filter by type kind (e.g., `structure`).
-- `parentTypeId`: Filter by parent type.
-- `visibility`: Filter by visibility.
+List/detail responses **enrich** each row in memory with:
-### Get Type Details
-`GET /api/types/:id`
+- `structure_fields`, `enum_values`, `flag_values`
-Returns the full type definition, including:
-- Enum values (if enum)
-- Flag values (if flags)
-- Structure fields (if structure)
-- Cast definitions
+`type_casts` are loaded into the server cache object but **not** attached per type in JSON responses (only the three relations above).
-### Create Type
-`POST /api/types`
+### `GET /api/types`
-Creates a new type. Supports atomic creation of children (enums/flags/fields).
+**Query** (all optional, exact match filters):
+
+- `kind` — e.g. `structure`, `enum`, `field`
+- `parentTypeId` — types whose `parent_type_id` equals this id
+- `visibility` — `public` \| `private` \| `custom`
+
+**Response header:** `X-Cache: HIT` \| `MISS` (server aggregate cache).
+
+Returns a **sorted** array (by `name`).
+
+### `GET /api/types/:id`
+
+Returns one enriched type, or **404** `{ "error": "Type not found" }` if the id is missing from the cache map (same backing data as the list endpoint).
+
+### `POST /api/types`
+
+Body uses **snake_case** keys matching persistence, e.g.:
-**Body Payload:**
```json
{
- "name": "MyType",
+ "name": "MyEnum",
"kind": "enum",
- "description": "A custom enum",
+ "description": "…",
"visibility": "public",
- "enumValues": [
- { "value": "A", "label": "Option A" },
- { "value": "B", "label": "Option B" }
+ "enum_values": [
+ { "value": "a", "label": "A", "order": 0 },
+ { "value": "b", "label": "B", "order": 1 }
]
}
```
-### Update Type
-`PATCH /api/types/:id`
+Structures can send `structure_fields` in the same request; nested `field` types are usually created first (see app flow).
-Updates metadata (name, description, visibility, etc.).
-*Note: Child modifications (adding fields/enums) should be handled via separate specific updates or by implementing deep update logic.*
+**Auth:** If `Authorization: Bearer ` is present and resolves to a user, **`owner_id`** is set on the new row from that user.
-### Delete Type
-`DELETE /api/types/:id`
+**Response:** **200** with the full enriched type (same shape as GET by id).
-Deletes a type and cascades to its children (values, fields).
+### `PATCH /api/types/:id`
-## Primitive Types
+**Core row** (`types` table): the handler updates only **`name`**, **`description`**, **`json_schema`**, **`visibility`**, **`meta`**, **`settings`** — read from the parsed body. Because the server forwards those properties explicitly, keys **missing** from JSON arrive as **`undefined`** in JS; verify your client / PostgREST behavior (unintended **`null`** clears are possible if you PATCH with a sparse body). The app’s `updateType` client usually sends a merged object for `meta` when needed.
-The system is seeded with the following primitive types (immutable system types):
-- `bool`
-- `int`
-- `float`
-- `string`
-- `array`
-- `object`
-- `enum`
-- `flags`
-- `reference`
-- `alias`
+**Not** updated via this path: **`kind`**, **`parent_type_id`**, **`owner_id`** (change those only with direct DB / a different API if added later).
-- `alias`
+**Child tables** (only when the corresponding key is present and is an array):
-## Caching & Consistency
+| Key | Behavior |
+|-----|----------|
+| `structure_fields` | **Replace** all rows for this structure (delete by `structure_type_id`, then insert). |
+| `enum_values` | **Replace** all rows for this enum type. |
+| `flag_values` | **Replace** all rows for this flags type. |
+| `fieldsToDelete` | After the above, **`deleteType`** each id (e.g. orphaned **`field`** types). |
-The Type System uses a high-performance in-memory caching layer (`AppCache`) to ensure fast read access.
+**Response:** **200** with the full enriched type after `flushTypeCache()` and `appCache.notify('type', id, 'update')`.
-- **Read Operations**: Responses for `/api/types` are cached with a 5-minute TTL.
-- **Write Operations**: Creating, Updating, or Deleting types immediately **invalidates** the cache.
-- **Real-time Updates**: Clients connected to the SSE stream (`/api/stream`) receive `app-update` events (type: `types`), allowing UIs to refresh schemas instantly without manual reloading.
+### `DELETE /api/types/:id`
+
+Deletes the **`types`** row (CASCADE removes `type_structure_fields` links). If the deleted row was a **structure**, the server then **best-effort deletes** the former `field_type_id` entries (no longer referenced). Failures on that second step are logged but do not roll back the main delete.
+
+**Response:** **200** `{ "success": true }`, plus cache flush / notify.
+
+---
+
+## Caching
+
+- Server: in-memory cache for the types aggregate (`getTypeState` in `db-types.ts`), TTL **5 minutes**; **create / update / delete** call **`flushTypeCache()`** and **`appCache.notify('type', id, …)`** for downstream listeners.
+- Client: `fetchWithDeduplication` / invalidation in `src/modules/types/client-types.ts` (`invalidateCache` on create/update/delete).
+
+---
+
+## Client modules (pm-pics)
+
+| Area | Location |
+|------|----------|
+| Fetch / CRUD | `src/modules/types/client-types.ts` |
+| Schema + UI generation | `src/modules/types/schema-utils.ts` — `generateSchemaForType`, `generateUiSchemaForType`, `deepMergeUiSchema` |
+| Editor shell | `src/modules/types/TypesEditor.tsx` — visual vs detail mode, save handlers |
+| Visual builder | `src/modules/types/TypeBuilder.tsx`, `builder/TypeBuilderContent.tsx` |
+| JSON / preview | `src/modules/types/TypeRenderer.tsx` |
+| RJSF widgets/templates | `src/modules/types/RJSFTemplates.tsx` |
+
+### Generated JSON Schema & UI schema
+
+- For `structure` / `enum` / `flags`, the UI builds from **`generateSchemaForType`** / **`generateUiSchemaForType`**, then merges **`meta.uiSchema`** from the type with `deepMergeUiSchema` (structure-level overrides win on conflicts).
+- **`meta.uiSchema`** holds RJSF keys: per-property widgets, `ui:group` for sections, nested `items` for arrays, root `ui:classNames`, etc.
+- Field-level customization also lives on the **`field`** type: `field.meta.uiSchema` and `field.settings` (group, defaults, `items_type_id`). The visual builder merges structure `meta.uiSchema[fieldName]` with the field type when loading so JSON-tab edits round-trip.
+
+### Field parent resolution (critical)
+
+- Palette drops store `refId` = chosen type id. When loading from DB, **`structureFieldToBuilderElement`** sets `type` and **`refId`** from `field.parent_type_id`** so saves resolve the value type, not the `Struct.field` name string.
+- **`generateSchemaForType`** treats `kind === 'field'` as a transparent wrapper and resolves through `parent_type_id`.
+
+### Forms & groups
+
+- Custom **`ObjectFieldTemplate`** groups properties by `ui:group` (collapsible sections).
+- Default widgets: enum → `select`, flags → `checkboxes`, custom widgets registered in `customWidgets`.
+
+---
+
+## Primitive types (seed)
+
+Typical seeds include: `string`, `int`, `float`, `bool`, `array`, `object`, `enum`, `flags`, `reference`, `alias`.
+
+Using the **primitive** `enum` / `flags` in a structure yields **empty** enums until you use a **custom** `enum` / `flags` type with values defined — custom types carry `enum_values` / `flag_values` in API responses.
+
+---
+
+## Access control (RLS)
+
+Row Level Security applies on the underlying tables. Typical intent:
+
+- **Read**: `public` types visible broadly; `private` / `custom` restricted by owner/admin rules (see policies in Supabase).
+- **Write**: owners and admins per product rules.
+
+Exact policies live with the database migrations; treat this section as behavioral summary, not the source of truth for SQL.
diff --git a/packages/ui/src/components/admin/ProductTypeDataForms.tsx b/packages/ui/src/components/admin/ProductTypeDataForms.tsx
new file mode 100644
index 00000000..6eba80b3
--- /dev/null
+++ b/packages/ui/src/components/admin/ProductTypeDataForms.tsx
@@ -0,0 +1,108 @@
+import React, { useMemo } from 'react';
+import Form from '@rjsf/core';
+import validator from '@rjsf/validator-ajv8';
+import { rjsfWidgetRegistry } from '@/modules/types/rjsfWidgetRegistry';
+import { customTemplates } from '@/modules/types/RJSFTemplates';
+import { generateSchemaForType, generateUiSchemaForType, deepMergeUiSchema } from '@/modules/types/schema-utils';
+import type { TypeDefinition } from '@/modules/types/client-types';
+import { Label } from '@/components/ui/label';
+import { T } from '@/i18n';
+
+const SingleTypeForm = ({
+ typeId,
+ typeDef,
+ types,
+ formData,
+ onChange,
+ disabled,
+}: {
+ typeId: string;
+ typeDef: TypeDefinition;
+ types: TypeDefinition[];
+ formData: Record;
+ onChange: (fd: Record) => void;
+ disabled?: boolean;
+}) => {
+ const jsonSchema = useMemo(() => generateSchemaForType(typeId, types), [typeId, types]);
+ const uiSchema = useMemo(() => {
+ const gen = generateUiSchemaForType(typeId, types);
+ return deepMergeUiSchema(gen, typeDef.meta?.uiSchema || {});
+ }, [typeId, types, typeDef.meta?.uiSchema]);
+
+ return (
+
+
+
+
+ );
+};
+
+export interface ProductTypeDataFormsProps {
+ /** Structure (and other) type ids configured for this product */
+ typeIds: string[];
+ /** Full type registry from `fetchTypes()` — required so `generateSchemaForType` can resolve fields (enums, primitives, field rows, nested structures). */
+ types: TypeDefinition[];
+ /** Per-type id → form payload (structure field values) */
+ value: Record>;
+ onChange: (typeId: string, formData: Record) => void;
+ disabled?: boolean;
+}
+
+/**
+ * Renders one @rjsf form per assigned type so product settings can hold structured payloads
+ * (e.g. pricing dimensions) defined in the type system.
+ */
+export const ProductTypeDataForms: React.FC = ({
+ typeIds,
+ types,
+ value,
+ onChange,
+ disabled,
+}) => {
+ const resolved = useMemo(() => {
+ return typeIds
+ .map((id) => ({ id, def: types.find((t) => t.id === id) }))
+ .filter((x): x is { id: string; def: TypeDefinition } => !!x.def);
+ }, [typeIds, types]);
+
+ if (resolved.length === 0) {
+ return (
+
+ Assign structure types in Product Settings to edit type-based fields here.
+