@cxtms/cx-schema 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.claude/skills/cx-core/SKILL.md +93 -0
  2. package/.claude/skills/cx-core/ref-entity-accounting.md +173 -0
  3. package/.claude/skills/cx-core/ref-entity-commodity.md +205 -0
  4. package/.claude/skills/cx-core/ref-entity-contact.md +153 -0
  5. package/.claude/skills/cx-core/ref-entity-geography.md +119 -0
  6. package/.claude/skills/cx-core/ref-entity-job.md +77 -0
  7. package/.claude/skills/cx-core/ref-entity-order-sub.md +140 -0
  8. package/.claude/skills/cx-core/ref-entity-order.md +168 -0
  9. package/.claude/skills/cx-core/ref-entity-rate.md +174 -0
  10. package/.claude/skills/cx-core/ref-entity-shared.md +147 -0
  11. package/.claude/skills/cx-core/ref-entity-warehouse.md +110 -0
  12. package/.claude/skills/cx-module/SKILL.md +402 -0
  13. package/.claude/skills/cx-module/ref-components-data.md +286 -0
  14. package/.claude/skills/cx-module/ref-components-display.md +394 -0
  15. package/.claude/skills/cx-module/ref-components-forms.md +362 -0
  16. package/.claude/skills/cx-module/ref-components-interactive.md +306 -0
  17. package/.claude/skills/cx-module/ref-components-layout.md +295 -0
  18. package/.claude/skills/cx-module/ref-components-specialized.md +427 -0
  19. package/.claude/skills/cx-workflow/SKILL.md +330 -0
  20. package/.claude/skills/cx-workflow/ref-accounting.md +66 -0
  21. package/.claude/skills/cx-workflow/ref-communication.md +161 -0
  22. package/.claude/skills/cx-workflow/ref-entity.md +162 -0
  23. package/.claude/skills/cx-workflow/ref-expressions.md +239 -0
  24. package/.claude/skills/cx-workflow/ref-filetransfer.md +80 -0
  25. package/.claude/skills/cx-workflow/ref-flow.md +180 -0
  26. package/.claude/skills/cx-workflow/ref-other.md +120 -0
  27. package/.claude/skills/cx-workflow/ref-query.md +85 -0
  28. package/.claude/skills/cx-workflow/ref-utilities.md +192 -0
  29. package/dist/cli.js +252 -33
  30. package/dist/cli.js.map +1 -1
  31. package/package.json +3 -2
  32. package/schemas/workflows/tasks/action-event.json +65 -0
  33. package/schemas/workflows/tasks/all.json +126 -26
  34. package/schemas/workflows/tasks/appmodule.json +56 -0
  35. package/schemas/workflows/tasks/attachment.json +4 -1
  36. package/schemas/workflows/tasks/authentication.json +72 -0
  37. package/schemas/workflows/tasks/caching.json +68 -0
  38. package/schemas/workflows/tasks/charge.json +3 -1
  39. package/schemas/workflows/tasks/commodity.json +3 -0
  40. package/schemas/workflows/tasks/contact-address.json +72 -0
  41. package/schemas/workflows/tasks/contact-payment-method.json +72 -0
  42. package/schemas/workflows/tasks/edi.json +65 -0
  43. package/schemas/workflows/tasks/filetransfer.json +102 -0
  44. package/schemas/workflows/tasks/flow-transition.json +68 -0
  45. package/schemas/workflows/tasks/httpRequest.json +23 -0
  46. package/schemas/workflows/tasks/import.json +64 -0
  47. package/schemas/workflows/tasks/inventory.json +67 -0
  48. package/schemas/workflows/tasks/movement.json +54 -0
  49. package/schemas/workflows/tasks/note.json +59 -0
  50. package/schemas/workflows/tasks/number.json +65 -0
  51. package/schemas/workflows/tasks/order.json +8 -1
  52. package/schemas/workflows/tasks/pdf-document.json +60 -0
  53. package/schemas/workflows/tasks/user.json +70 -0
  54. package/scripts/postinstall.js +2 -2
@@ -0,0 +1,147 @@
1
+ # Shared Entity Reference
2
+
3
+ Tag, Attachment, Division, EquipmentType, PackageType, Note/NoteThread.
4
+
5
+ ## Tag
6
+
7
+ Tagging system for orders, commodities, inventory items.
8
+
9
+ | Field | Type | Notes |
10
+ |-------|------|-------|
11
+ | `tagId` | `int` | PK |
12
+ | `organizationId` | `int` | |
13
+ | `name` | `string` | |
14
+ | `description` | `string?` | |
15
+ | `entityName` | `string` | Discriminator: which entity type this tag belongs to |
16
+ | `isDeleted` | `bool` | Soft delete |
17
+ | `customValues` | `Dictionary` | jsonb |
18
+
19
+ **Join entities** (all have own `customValues`):
20
+ - `OrderTag` — `orderId` + `tagId` + `customValues`
21
+ - `CommodityTag` — `commodityId` + `tagId` + `customValues`
22
+ - `InventoryItemTag` — `inventoryItemId` + `tagId` + `customValues`
23
+
24
+ ---
25
+
26
+ ## Attachment
27
+
28
+ File attachments linked to orders, contacts, jobs, etc.
29
+
30
+ | Field | Type | Notes |
31
+ |-------|------|-------|
32
+ | `attachmentId` | `int` | PK |
33
+ | `attachmentGuid` | `Guid?` | |
34
+ | `fileName` | `string` | |
35
+ | `fileUri` | `string` | Storage path |
36
+ | `fileExtension` | `string` | Computed from fileName |
37
+ | `previewUri` | `string?` | |
38
+ | `thumbnailUri` | `string?` | |
39
+ | `description` | `string?` | |
40
+ | `attachmentType` | `AttachmentType` enum | Picture=1, OtherDocument, Avatar, CustomerDocument |
41
+ | `parentId` | `string?` | Polymorphic FK (entity ID as string) |
42
+ | `parentType` | `AttachmentParentType` enum | None=0, Order=1, Contact=2, AccountingTransaction=3, EquipmentType=4, Job=5, Commodity=6 |
43
+ | `status` | `AttachmentStatus` enum | Active=0, PendingUpload=1, UploadFailed=2 |
44
+ | `category` | `AttachmentCategory` enum | General=0, FieldValue=1 |
45
+ | `organizationId` | `int` | |
46
+ | `customValues` | `Dictionary` | jsonb |
47
+
48
+ ### GraphQL Computed
49
+
50
+ - `isImage`, `isPdf` — computed from extension
51
+ - `presignedFileUri`, `presignedPreviewUri`, `presignedThumbnailUri` — signed URLs
52
+ - `getPresignedUri(expiresInDays, uriType)` — custom resolver
53
+ - `getParentOrder` — resolve parent Order
54
+
55
+ ---
56
+
57
+ ## Division
58
+
59
+ Organization divisions/branches.
60
+
61
+ | Field | Type | Notes |
62
+ |-------|------|-------|
63
+ | `divisionId` | `int` | PK |
64
+ | `organizationId` | `int` | |
65
+ | `divisionName` | `string` | |
66
+ | `email` | `string?` | |
67
+ | `phoneNumber` | `string?` | |
68
+ | `faxNumber` | `string?` | |
69
+ | `streetAndNumber` | `string?` | |
70
+ | `city` | `string?` | |
71
+ | `stateCode` | `string?` | FK to State |
72
+ | `countryCode` | `string?` | FK to Country |
73
+ | `zipCode` | `string?` | |
74
+ | `portId` | `string?` | FK to Port |
75
+ | `comments` | `string?` | |
76
+ | `parentDivisionId` | `int?` | Self-referencing FK |
77
+ | `assignDivisionToEntities` | `bool` | |
78
+ | `useDivisionInDocumentHeaders` | `bool` | |
79
+ | `airAmsOriginatorCode` | `string?` | |
80
+
81
+ **Navigation:** `country`, `state`, `port`, `parentDivision`, `nestedDivisions` (children). No customValues.
82
+
83
+ ---
84
+
85
+ ## EquipmentType
86
+
87
+ | Field | Type | Notes |
88
+ |-------|------|-------|
89
+ | `equipmentTypeId` | `int` | PK |
90
+ | `organizationId` | `int` | |
91
+ | `name` | `string` | |
92
+
93
+ No customValues. Linked to carriers via `CarrierEquipment` join.
94
+
95
+ ---
96
+
97
+ ## PackageType
98
+
99
+ | Field | Type | Notes |
100
+ |-------|------|-------|
101
+ | `packageTypeId` | `int` | PK |
102
+ | `organizationId` | `int` | |
103
+ | `name` | `string` | |
104
+ | `height` | `decimal` | |
105
+ | `length` | `decimal` | |
106
+ | `width` | `decimal` | |
107
+ | `weight` | `decimal` | |
108
+ | `maximumWeight` | `decimal` | |
109
+ | `volume` | `decimal` | |
110
+ | `air` | `bool` | Mode applicability |
111
+ | `ground` | `bool` | |
112
+ | `ocean` | `bool` | |
113
+ | `containerDescriptionCode` | `string?` | FK |
114
+ | `containerTypeCode` | `string?` | FK |
115
+ | `packageCategoryCode` | `string` | FK |
116
+
117
+ **Navigation:** `packageCategory`, `containerDescription`, `containerType`. No customValues.
118
+
119
+ ---
120
+
121
+ ## NoteThread
122
+
123
+ | Field | Type | Notes |
124
+ |-------|------|-------|
125
+ | `id` | `Guid` | PK |
126
+ | `organizationId` | `int` | |
127
+ | `name` | `string` | |
128
+ | `slug` | `string` | Lowercase identifier |
129
+ | `isDeleted` | `bool` | Soft delete |
130
+ | `metadata` | `Dictionary` | jsonb (non-nullable, defaults to {}) |
131
+
132
+ **Collections:** `notes` (one-to-many)
133
+
134
+ ## Note
135
+
136
+ | Field | Type | Notes |
137
+ |-------|------|-------|
138
+ | `id` | `Guid` | PK |
139
+ | `threadId` | `Guid` | FK to NoteThread |
140
+ | `threadName` | `string` | Snapshot of thread name |
141
+ | `content` | `Dictionary` | TipTap document format (root type="doc"), max 256KB |
142
+ | `mentions` | `[Dictionary]?` | Mentioned users/entities |
143
+ | `tags` | `[string]` | Max 20 tags, each max 32 chars |
144
+ | `isPinned` | `bool` | |
145
+ | `isDeleted` | `bool` | Soft delete |
146
+
147
+ **Navigation:** `thread` (NoteThread)
@@ -0,0 +1,110 @@
1
+ # Warehouse & Inventory Entity Reference
2
+
3
+ ## InventoryItem
4
+
5
+ SKU-level inventory tracking.
6
+
7
+ | Field | Type | Notes |
8
+ |-------|------|-------|
9
+ | `inventoryItemId` | `int` | PK |
10
+ | `organizationId` | `int` | |
11
+ | `sku` | `string` | Stock keeping unit |
12
+ | `productName` | `string?` | |
13
+ | `modelNumber` | `string?` | |
14
+ | `description` | `string?` | |
15
+ | `availableQuantity` | `int` | |
16
+ | `backOrderQuantity` | `int` | |
17
+ | `height` | `decimal?` | |
18
+ | `length` | `decimal?` | |
19
+ | `width` | `decimal?` | |
20
+ | `weight` | `decimal?` | |
21
+ | `volumePiece` | `decimal?` | |
22
+ | `dimensionsUnit` | `DimensionsUnit` enum | In, Cm, M, Ft |
23
+ | `weightUnit` | `WeightUnit` enum | Lb, Kg |
24
+ | `volumeUnit` | `VolumeUnit` enum | Ft, Vlb, Vkg, M, In, Cm |
25
+ | `useSerialNumbers` | `bool` | |
26
+ | `isInactive` | `bool` | |
27
+ | `customerContactId` | `int?` | FK to Contact |
28
+ | `manufacturerContactId` | `int?` | FK to Contact |
29
+ | `packageTypeId` | `int?` | FK to PackageType |
30
+ | `customValues` | `Dictionary` | jsonb |
31
+
32
+ ### Navigation
33
+
34
+ | Field | Type |
35
+ |-------|------|
36
+ | `customerContact` | `Contact?` |
37
+ | `manufacturerContact` | `Contact?` |
38
+ | `packageType` | `PackageType?` |
39
+ | `commodities` | `[Commodity]` |
40
+ | `inventoryItemTags` | `[InventoryItemTag]` |
41
+ | `allTags` | `[InventoryItemAllTagsView]` |
42
+
43
+ ### GraphQL Computed
44
+
45
+ - `getContact(idPropertyName)` — resolve contact from customValues
46
+
47
+ ---
48
+
49
+ ## WarehouseLocation
50
+
51
+ Physical storage zones/locations in warehouse.
52
+
53
+ | Field | Type | Notes |
54
+ |-------|------|-------|
55
+ | `warehouseLocationId` | `int` | PK |
56
+ | `organizationId` | `int` | |
57
+ | `code` | `string` | Location code |
58
+ | `description` | `string?` | |
59
+ | `locationType` | `LocationType` enum | See below |
60
+ | `height` | `decimal?` | |
61
+ | `length` | `decimal?` | |
62
+ | `width` | `decimal?` | |
63
+ | `maximumWeight` | `decimal?` | |
64
+ | `isInactive` | `bool` | |
65
+ | `customerId` | `int?` | FK to Contact |
66
+ | `parentZoneId` | `int?` | FK to WarehouseZone |
67
+
68
+ ### LocationType Enum
69
+
70
+ Receiving, Storage, Replenishment, Picking, QualityControl, Shipping, Mobile, Other, Packing, Service, PutAway
71
+
72
+ ### Navigation
73
+
74
+ `customer` (Contact), `parentWarehouseZone` (WarehouseZone), `commodities` ([Commodity]). No customValues.
75
+
76
+ ---
77
+
78
+ ## CargoMovement
79
+
80
+ **Not a separate entity** — implemented as `Order` with `orderType = CargoMovement` (value 7).
81
+
82
+ Movement-specific data stored in Order's `customValues`:
83
+
84
+ | CustomValues Key | Type | Notes |
85
+ |-----------------|------|-------|
86
+ | `movementStatus` | `string` | e.g., "Created" |
87
+ | `movementType` | `string` | |
88
+ | `destinationLocationId` | `string/int` | Warehouse location ID |
89
+ | `destinationLocationDescription` | `string` | |
90
+ | `transportationMode` | `string` | |
91
+ | `finalMileCarrier` | `string` | |
92
+
93
+ The Order's `trackingNumber` field serves as the pallet number for cargo movements.
94
+
95
+ ```yaml
96
+ # Example: Query cargo movement in workflow
97
+ - task: "Query/GraphQL@1"
98
+ name: GetMovement
99
+ inputs:
100
+ query: >-
101
+ query($id: Int!, $orgId: Int!) {
102
+ order(organizationId: $orgId, orderId: $id) {
103
+ orderId orderNumber trackingNumber orderType
104
+ customValues
105
+ }
106
+ }
107
+ variables:
108
+ id: "{{ int inputs.orderId }}"
109
+ orgId: "{{ int organizationId }}"
110
+ ```
@@ -0,0 +1,402 @@
1
+ ---
2
+ name: cx-module
3
+ description: Generate schema-valid CargoXplorer app module YAML files (UI screens, forms, grids, routes)
4
+ argument-hint: <description of what to build>
5
+ ---
6
+
7
+ You are a CargoXplorer module YAML builder. You generate schema-valid YAML for CX app modules — UI screens, forms, data grids, routes, and components. All output must conform to the JSON schemas in `.cx-schema/`.
8
+
9
+ **IMPORTANT — use `cx-cli` for all module operations:**
10
+ - **Scaffold**: `npx cx-cli create module <name> --template <template>` — generates a schema-valid YAML file. ALWAYS run this first, then read the generated file, then customize. Do NOT write YAML from scratch or copy templates manually.
11
+ - **Scaffold with fields**: `npx cx-cli create module <name> --template <template> --options '<json>'`
12
+ - **Validate**: `npx cx-cli <file.yaml>` — run after every change
13
+ - **Schema lookup**: `npx cx-cli schema <component>` — e.g., `cx-cli schema form`, `cx-cli schema dataGrid`
14
+ - **Examples**: `npx cx-cli example <component>` — show example YAML
15
+ - **List schemas**: `npx cx-cli list`
16
+ - **Extract**: `npx cx-cli extract <source> <component> --to <target>` — move components between modules
17
+ - **Feature folder**: `npx cx-cli create module <name> --template <template> --feature <feature-name>`
18
+
19
+ ## Generation Workflow
20
+
21
+ ### Step 1: Scaffold via CLI — MANDATORY
22
+
23
+ **You MUST run `cx-cli create module` to generate the initial file.** Do not skip this step. Do not write YAML from scratch. Do not read template files and copy them manually. The CLI generates correct UUIDs, file paths, and structure.
24
+
25
+ ```bash
26
+ npx cx-cli create module <name> --template <template>
27
+ npx cx-cli create module <name> --template <template> --options '<json>'
28
+ ```
29
+
30
+ | Template | Use Case |
31
+ |----------|----------|
32
+ | `default` | Generic module with form |
33
+ | `form` | Entity create/edit form |
34
+ | `configuration` | Settings/config screen |
35
+ | `grid` | List/table view |
36
+ | `select` | Reusable async select |
37
+
38
+ ### Step 2: Read the generated file
39
+
40
+ ### Step 3: Customize for the use case
41
+
42
+ **All templates** — update module name, component names, entity fields, permissions, GraphQL queries/mutations.
43
+
44
+ **`form`** — update form fields, validationSchema, query/mutation field lists. Add tabs for grouped fields. Customize toolbar buttons. Update dirtyGuard messages.
45
+
46
+ **`configuration`** — update form fields, initialValues.append defaults, validationSchema rules, query/mutation field lists. Add tabs for grouped settings.
47
+
48
+ **`grid`** — update view columns, filters, entity fields. Add/remove views. Customize dotsMenu actions. Configure toolbar with export/import actions.
49
+
50
+ **`select`** — update `valueFieldName`, `itemLabelTemplate`, `itemValueTemplate`. Customize GraphQL query fields and variables. Set `navigateActionPermission`. Configure `dropDownToolbar` create button dialog.
51
+
52
+ ### Step 4: Validate
53
+
54
+ ```bash
55
+ npx cx-cli <generated-file.yaml>
56
+ ```
57
+
58
+ ---
59
+
60
+ ## --options Flag
61
+
62
+ Customize generated modules at scaffold time with `--options`. Accepts inline JSON or a file path.
63
+
64
+ ### Field Array Format (all templates)
65
+
66
+ ```bash
67
+ npx cx-cli create module "Tariff" --template grid --options '[
68
+ {"name": "code", "type": "text", "label": "Tariff Code", "required": true},
69
+ {"name": "rate", "type": "number", "label": "Rate %"},
70
+ {"name": "effectiveDate", "type": "date"},
71
+ {"name": "isActive", "type": "checkbox", "label": "Active"}
72
+ ]'
73
+ ```
74
+
75
+ ### Object Format (with entityName)
76
+
77
+ ```bash
78
+ npx cx-cli create module "Country" --template select --options '{
79
+ "entityName": "Country",
80
+ "fields": [
81
+ {"name": "countryCode", "type": "text", "label": "Country Code"},
82
+ {"name": "countryName", "type": "text", "label": "Country Name"}
83
+ ]
84
+ }'
85
+ ```
86
+
87
+ ### Field Properties
88
+
89
+ | Property | Type | Description |
90
+ |----------|------|-------------|
91
+ | `name` | string | **Required.** Field name (camelCase) |
92
+ | `type` | string | **Required.** text, number, checkbox, date, select, select-async, textarea, email |
93
+ | `label` | string | Display label (auto-generated from name if omitted) |
94
+ | `required` | boolean | Add to validationSchema as required |
95
+ | `default` | any | Default value (form/configuration template: added to initialValues.append) |
96
+
97
+ ### What --options Customizes Per Template
98
+
99
+ | Template | Fields | Entity | Queries |
100
+ |----------|--------|--------|---------|
101
+ | `form` | Form children, validationSchema, initialValues.append | Entity field definitions | GraphQL query field lists (preserves `id`) |
102
+ | `configuration` | Form children, validationSchema, initialValues.append | Entity field definitions | GraphQL query field lists |
103
+ | `grid` | View columns (with showAs by type), entity fields | Entity name + rootEntityName | — |
104
+ | `select` | Entity fields, itemLabelTemplate | Entity name | GraphQL query field lists |
105
+
106
+ ---
107
+
108
+ ## Extract Command
109
+
110
+ Move a component (and its routes) from one module into another. Useful for splitting large modules into smaller, focused ones.
111
+
112
+ ```bash
113
+ cx-cli extract <source-file> <component-name> --to <target-file>
114
+ ```
115
+
116
+ ### What Gets Moved
117
+ - The component matching the exact `name` field
118
+ - Any routes whose `component` field matches the component name
119
+ - Permissions and entities are **NOT** moved
120
+
121
+ ### Examples
122
+
123
+ ```bash
124
+ # Extract to a new file (creates module scaffold automatically)
125
+ npx cx-cli extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml
126
+
127
+ # Extract to an existing module
128
+ npx cx-cli extract modules/main.yaml Dashboard --to modules/dashboard.yaml
129
+ ```
130
+
131
+ ### New Target Scaffold
132
+ When the target file doesn't exist, a new module is created with:
133
+ - `module` name derived from filename (PascalCase)
134
+ - Fresh `appModuleId` (UUID)
135
+ - `application` copied from source
136
+ - Empty `entities` and `permissions` arrays
137
+
138
+ ### Workflow
139
+ 1. Run `extract` to move the component
140
+ 2. Manually move any related permissions/entities if needed
141
+ 3. Validate both files: `npx cx-cli <source>` and `npx cx-cli <target>`
142
+
143
+ ---
144
+
145
+ ## On-Demand References
146
+
147
+ **Read these files only when needed for the current task.** Do not load all references upfront.
148
+
149
+ ### Entity Field Reference (cx-core)
150
+
151
+ !cat .claude/skills/cx-core/SKILL.md
152
+
153
+ ### Component Directory
154
+
155
+ Read the relevant category ref file when building specific component types:
156
+
157
+ | Category | Components | File |
158
+ |----------|-----------|------|
159
+ | **Layout & Structure** | `layout`, `row`, `col`, `header`, `tabs`, `toolbar`, `card`, `line` | `.claude/skills/cx-module/ref-components-layout.md` |
160
+ | **Forms & Input** | `form`, `field`, `field-collection`, `barcodeScanner` | `.claude/skills/cx-module/ref-components-forms.md` |
161
+ | **Data Display** | `dataGrid`, `text`, `markup`, `badge`, `icon`, `image`, `photo`, `summary`, `diff`, `viewer`, `embed` | `.claude/skills/cx-module/ref-components-display.md` |
162
+ | **Interactive & Nav** | `button`, `dropdown`, `menuButton`, `link`, `redirect`, `navbar`, `navbarItem`, `navbarLink`, `navDropdown` | `.claude/skills/cx-module/ref-components-interactive.md` |
163
+ | **Data & Collections** | `collection`, `list`, `listItem`, `datasource`, `script` | `.claude/skills/cx-module/ref-components-data.md` |
164
+ | **Specialized** | `calendar`, `notes`, `dashboard`, `dashboard-widget`, `widget`, `timeline`, `timeline-grid`, `oauth2` | `.claude/skills/cx-module/ref-components-specialized.md` |
165
+
166
+ ### Templates
167
+
168
+ Read the relevant template after scaffolding to understand the generated structure:
169
+
170
+ | Template | File |
171
+ |----------|------|
172
+ | default | `templates/module.yaml` |
173
+ | form | `templates/module-form.yaml` |
174
+ | configuration | `templates/module-configuration.yaml` |
175
+ | grid | `templates/module-grid.yaml` |
176
+ | select | `templates/module-select.yaml` |
177
+
178
+ ### JSON Schemas
179
+
180
+ Read schema files from `.cx-schema/` only when debugging validation errors:
181
+ - `schemas.json` — main schema definitions
182
+ - `components/<type>.json` — component schemas (layout, form, dataGrid, field, button, tabs, card, calendar, collection, appComponent, module)
183
+ - `fields/<type>.json` — field schemas (text, select, select-async)
184
+ - `actions/<type>.json` — action schemas (navigate, mutation, dialog, all)
185
+
186
+ ---
187
+
188
+ # Module YAML Reference
189
+
190
+ ## Top-Level Structure
191
+
192
+ ```yaml
193
+ module:
194
+ name: "<ModuleName>" # PascalCase identifier
195
+ appModuleId: "<uuid>" # Generate a new UUID v4
196
+ displayName:
197
+ en-US: "Human Readable Name"
198
+ description:
199
+ en-US: "Module description"
200
+ application: "CargoXplorer" # Required
201
+ fileName: "modules/<name>-module.yaml" # File path in repo
202
+
203
+ entities:
204
+ - name: <EntityName>
205
+ entityKind: Order | Contact | OrderEntity | AccountingTransaction | Calendar | CalendarEvent | Other
206
+ extension: false # true if extending existing entity
207
+ displayName: { en-US: "..." }
208
+ fields:
209
+ - name: fieldName
210
+ fieldType: text | number | date | boolean | ...
211
+ displayName: { en-US: "..." }
212
+ isCustomField: false
213
+ props:
214
+ allowOrderBy: true
215
+ allowFilter: true
216
+ filter: # Optional filter selector
217
+ component: "Contacts/Select"
218
+ props:
219
+ filter: "contactType: Customer"
220
+ options: { baseName: "contactId" }
221
+
222
+ permissions:
223
+ - name: "ModuleName/Read" # PascalCase with slashes
224
+ displayName: { en-US: "..." }
225
+ roles: ["Admin", "Manager"]
226
+
227
+ routes:
228
+ - name: "routeName"
229
+ path: "/module-path" # Supports :params
230
+ component: ComponentName # References component name
231
+ platforms: [web, mobile] # Optional, defaults to both
232
+ props:
233
+ title: { en-US: "..." }
234
+ icon: "icon-name"
235
+ permission: "permission-name"
236
+
237
+ components:
238
+ - name: "ModuleName/ComponentName" # Pattern: Module/Component
239
+ displayName: { en-US: "..." }
240
+ permissions: "permission-name" # String or array
241
+ layout:
242
+ component: layout # Root must be a component
243
+ # ... component tree
244
+ ```
245
+
246
+ ## Action Types
247
+
248
+ Actions are used in event handlers (onClick, onSubmit, etc.) as arrays:
249
+
250
+ ```yaml
251
+ onClick:
252
+ - navigate: "~/path/{{ id }}" # Navigate to route
253
+ - navigateBack: { fallback: "/home" } # Go back in history
254
+ - navigateBackOrClose: { fallback: "/home" } # Go back or close dialog
255
+ - refresh: "componentName" # Refresh a component
256
+ - notification: { message: { en-US: "Saved!" }, type: success } # success|error|warning|info
257
+ - confirm: { title: { en-US: "Delete?" }, message: { en-US: "Are you sure?" } }
258
+ - mutation:
259
+ command: "mutation M($input: MInput!) { m(input: $input) { result } }"
260
+ variables: { input: "{{ form }}" }
261
+ onSuccess: [...]
262
+ onError: [...]
263
+ - query:
264
+ command: "query Q($id: ID!) { entity(id: $id) { id name } }"
265
+ variables: { id: "{{ entityId }}" }
266
+ onSuccess: [...]
267
+ onError: [...]
268
+ - setFields: { "fieldName": "{{ value }}" } # Set form field values
269
+ - setStore: { "key": "{{ value }}" } # Set store values
270
+ - validateForm: {} # Trigger form validation
271
+ - dialog:
272
+ name: "dialogName"
273
+ props: { title: { en-US: "Title" } }
274
+ component: { component: "Module/Component" }
275
+ onClose: [...]
276
+ - workflow:
277
+ workflowId: "<uuid>"
278
+ inputs: { key: "value" }
279
+ onSuccess: [...]
280
+ onError: [...]
281
+ - fileDownload: { url: "...", fileName: "..." }
282
+ - forEach:
283
+ items: "{{ selectedItems }}"
284
+ item: "currentItem"
285
+ actions: [...]
286
+ - if: "{{ condition }}"
287
+ then: [...]
288
+ else: [...]
289
+ - consoleLog: { message: "debug info" }
290
+ - openBarcodeScanner: { onScan: [...] }
291
+ - resetDirtyState: {}
292
+ ```
293
+
294
+ ## Common Patterns
295
+
296
+ ### Localized strings
297
+ ```yaml
298
+ displayName:
299
+ en-US: "English text"
300
+ es-ES: "Spanish text"
301
+ ```
302
+
303
+ ### Template expressions
304
+ ```yaml
305
+ value: "{{ fieldName }}" # Simple variable
306
+ value: "{{ format date L }}" # Format helper
307
+ value: "{{ number quantity }}" # Type cast
308
+ value: "{{ eval items.length > 0 }}" # JavaScript expression
309
+ isHidden: "{{ eval !canEdit }}" # Conditional visibility
310
+ ```
311
+
312
+ ### Permissions
313
+ ```yaml
314
+ permission: "ModuleName/Read" # Single string (PascalCase/Action)
315
+ permissions: # Array
316
+ - "ModuleName/Read"
317
+ - "ModuleName/Update"
318
+ ```
319
+
320
+ ### Async Select Component Pattern
321
+
322
+ Reusable select components (e.g., `Countries/Select`, `Ports/Select`) follow this structure:
323
+
324
+ ```yaml
325
+ - name: Entity/Select
326
+ displayName: { en-US: "Select Entity" }
327
+ platforms: [web, mobile]
328
+ layout:
329
+ component: field
330
+ name: entityId # Value binding field name
331
+ props:
332
+ type: select-async
333
+ label: { en-US: "Entity" }
334
+ options:
335
+ valueFieldName: "entityId" # Which result field holds the value
336
+ itemLabelTemplate: "{{name}}" # Handlebars template for labels
337
+ itemValueTemplate: "{{entityId}}" # Handlebars template for values
338
+ navigateActionPermission: "Entity/Update"
339
+ searchQuery: # References list query below
340
+ name: getEntities
341
+ path: entities.items
342
+ params:
343
+ search: "{{ string search }}"
344
+ take: "{{ number pageSize }}"
345
+ skip: "{{ number skip }}"
346
+ filter: "{{ string filter }}"
347
+ valueQuery: # References single-item query below
348
+ name: getEntity
349
+ path: entity
350
+ params:
351
+ entityId: "{{entityId}}"
352
+ allowSearch: true
353
+ allowClear: true
354
+ dropDownToolbar: # Create button in dropdown
355
+ - component: button
356
+ name: createBtn
357
+ props:
358
+ label: { en-US: "Create Entity" }
359
+ icon: plus
360
+ onClick:
361
+ - dialog:
362
+ component: Entity/CreateEntity
363
+ onClose:
364
+ - selectValue: "{{ result.entityId }}"
365
+ queries:
366
+ - name: getEntities # Paginated search query
367
+ query:
368
+ command: >-
369
+ query($organizationId: Int!, $filter: String!, $search: String!, $take: Int!, $skip: Int!) {
370
+ entities(...) { items { entityId name } totalCount }
371
+ }
372
+ variables: { organizationId: "{{number organizationId}}", ... }
373
+ - name: getEntity # Single-value lookup query
374
+ query:
375
+ command: >-
376
+ query($organizationId: Int!, $entityId: Int!) {
377
+ entity(...) { entityId name }
378
+ }
379
+ variables: { entityId: "{{number entityId}}" }
380
+ onEditClick: # Edit action on selected item
381
+ - dialog:
382
+ component: { layout: { component: layout, children: [{ component: Entity/UpdateEntity }] } }
383
+ ```
384
+
385
+ ---
386
+
387
+ # Generation Rules
388
+
389
+ 1. **Always scaffold via `cx-cli create module` first** — never write YAML from scratch, never copy templates manually
390
+ 2. **Use localized strings** `{ en-US: "..." }` for all user-visible text
391
+ 3. **Follow naming conventions**:
392
+ - Module names: PascalCase (e.g., `WarehouseLocations`)
393
+ - Component names: Module/Component pattern (e.g., `WarehouseLocations/List`)
394
+ - Route paths: kebab-case (e.g., `/warehouse-locations`)
395
+ - Permission names: PascalCase with slashes (e.g., `WarehouseLocations/Read`, `System/Contacts/Update`)
396
+ 4. **Template expressions** use `{{ expression }}` syntax (double curly braces)
397
+ 5. **Include fileName** property pointing to the YAML file location
398
+ 6. **Set proper entityKind** when defining entities (Order, Contact, OrderEntity, AccountingTransaction, Calendar, CalendarEvent, Other)
399
+ 7. **DataGrid options** requires ALL properties: query, rootEntityName, entityKeys, navigationType, enableDynamicGrid, enableViews, enableSearch, enablePagination, enableColumns, enableFilter, defaultView, onRowClick
400
+ 8. **Form component** requires `validationSchema` in props
401
+ 9. **Do not change `appModuleId` or `fileName`** — set correctly by CLI scaffold
402
+ 10. **Always validate** the final YAML: `npx cx-cli <file.yaml>`