@cxtms/cx-schema 1.9.258 → 1.9.260

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 (34) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/types.d.ts +1 -1
  4. package/dist/types.d.ts.map +1 -1
  5. package/dist/workflowValidator.d.ts +7 -0
  6. package/dist/workflowValidator.d.ts.map +1 -1
  7. package/dist/workflowValidator.js +54 -0
  8. package/dist/workflowValidator.js.map +1 -1
  9. package/package.json +1 -1
  10. package/schemas/actions/sound.json +4 -3
  11. package/schemas/actions/vibrate.json +6 -4
  12. package/schemas/components/README.md +1 -0
  13. package/schemas/components/avatar.json +117 -1
  14. package/schemas/components/infoLine.json +82 -1
  15. package/schemas/components/planner.json +369 -1
  16. package/schemas/components/progressBar.json +115 -1
  17. package/schemas/components/timeline.json +24 -0
  18. package/schemas/fields/attachment.json +35 -2
  19. package/schemas/workflows/agent/agent.json +98 -0
  20. package/schemas/workflows/tasks/attachment.json +58 -2
  21. package/schemas/workflows/workflow.json +14 -19
  22. package/skills/cxtms-developer/ref-entity-notification.md +2 -2
  23. package/skills/cxtms-developer/ref-entity-shared.md +6 -2
  24. package/skills/cxtms-module-builder/SKILL.md +83 -5
  25. package/skills/cxtms-module-builder/ref-components-data.md +1 -1
  26. package/skills/cxtms-module-builder/ref-components-display.md +98 -15
  27. package/skills/cxtms-module-builder/ref-components-forms.md +27 -1
  28. package/skills/cxtms-module-builder/ref-components-interactive.md +3 -3
  29. package/skills/cxtms-module-builder/ref-components-layout.md +1 -1
  30. package/skills/cxtms-module-builder/ref-components-specialized.md +3 -0
  31. package/skills/cxtms-workflow-builder/SKILL.md +3 -1
  32. package/skills/cxtms-workflow-builder/ref-agent.md +283 -0
  33. package/skills/cxtms-workflow-builder/ref-communication.md +37 -10
  34. package/templates/workflow-agent.yaml +48 -0
@@ -1 +1,369 @@
1
- {"$schema":"http://json-schema.org/draft-07/schema#","title":"Planner Component","type":"object","required":["component","props"],"properties":{"component":{"const":"planner"},"name":{"type":"string"},"props":{"type":"object","required":["queries","rows","columns","itemTemplate"],"properties":{"height":{},"refreshHandler":{"type":"string"},"debug":{"type":"boolean"},"options":{"type":"object","properties":{"enableVirtualization":{"type":"boolean"},"virtualizeThreshold":{"type":"number"},"estimatedRowHeight":{"type":"number"}},"additionalProperties":true},"queries":{"type":"array","minItems":1,"items":{"type":"object","required":["name","query"],"properties":{"name":{"type":"string"},"query":{"type":"object"},"itemsPath":{"type":"string"},"parentAs":{"type":"string"},"mapping":{"type":"object"}},"additionalProperties":true}},"rows":{"type":"object","required":["groupBy","headerWidth"],"properties":{"groupBy":{"type":"string"},"fromQuery":{"type":"string"},"key":{"type":"string"},"label":{},"orderBy":{"type":"array"},"unassigned":{"type":"object"},"headerWidth":{"type":"number"}},"additionalProperties":true},"columns":{"type":"object","required":["width"],"properties":{"orderBy":{"type":"array"},"appendEmpty":{"type":"boolean","description":"Keep one trailing empty slot beyond the largest group."},"label":{},"width":{"type":"number"}},"additionalProperties":true},"rowHeaderTemplate":{"$ref":"../schemas.json#/definitions/component"},"itemTemplate":{"$ref":"../schemas.json#/definitions/component"},"emptyCellTemplate":{"$ref":"../schemas.json#/definitions/component"},"summaryComponent":{"$ref":"../schemas.json#/definitions/component"},"emptyStateTemplate":{"$ref":"../schemas.json#/definitions/component"},"itemAccent":{},"itemGroups":{"type":"object","required":["key"],"properties":{"key":{"type":"string"},"headerTemplate":{"$ref":"../schemas.json#/definitions/component"},"accent":{},"dotsMenu":{"type":"object"}},"additionalProperties":false},"dragSource":{"type":"object","required":["type"],"properties":{"type":{"type":"string"},"data":{},"canDrag":{}},"additionalProperties":false},"events":{"type":"object","additionalProperties":{"$ref":"../schemas.json#/definitions/actionsList"}}},"additionalProperties":true}},"additionalProperties":true}
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Planner Component",
4
+ "type": "object",
5
+ "required": [
6
+ "component",
7
+ "props"
8
+ ],
9
+ "properties": {
10
+ "component": {
11
+ "const": "planner"
12
+ },
13
+ "name": {
14
+ "type": "string",
15
+ "description": "Component instance name for referencing in actions/variables"
16
+ },
17
+ "props": {
18
+ "type": "object",
19
+ "required": [
20
+ "queries",
21
+ "rows",
22
+ "columns",
23
+ "itemTemplate"
24
+ ],
25
+ "properties": {
26
+ "height": {
27
+ "default": 700,
28
+ "description": "Board height; drives the vertical scroll viewport"
29
+ },
30
+ "refreshHandler": {
31
+ "type": "string",
32
+ "description": "Named refresh channel: `- refresh: <this name>` re-runs the queries and rebuilds the board even when no query variable changed (e.g. after a mutation from an item template). Defaults to the component name.",
33
+ "x-example": "driverPlannerData"
34
+ },
35
+ "debug": {
36
+ "type": "boolean",
37
+ "default": false,
38
+ "description": "Log per-stage pipeline counts (extracted N -> M rows -> K columns, orphan count, resolved paths) through logger"
39
+ },
40
+ "options": {
41
+ "type": "object",
42
+ "properties": {
43
+ "enableVirtualization": {
44
+ "type": "boolean",
45
+ "default": true,
46
+ "description": "false forces plain (non-virtualised) rendering"
47
+ },
48
+ "virtualizeThreshold": {
49
+ "type": "number",
50
+ "default": 50,
51
+ "description": "Row count above which rows virtualise (TanStack Virtual, measured heights)"
52
+ },
53
+ "estimatedRowHeight": {
54
+ "type": "number",
55
+ "default": 220,
56
+ "description": "Initial row height estimate; real heights are measured after mount"
57
+ }
58
+ },
59
+ "additionalProperties": true
60
+ },
61
+ "queries": {
62
+ "type": "array",
63
+ "minItems": 1,
64
+ "items": {
65
+ "type": "object",
66
+ "required": [
67
+ "name",
68
+ "query"
69
+ ],
70
+ "properties": {
71
+ "name": {
72
+ "type": "string",
73
+ "description": "Query name; rows.fromQuery references the rows source by this name"
74
+ },
75
+ "query": {
76
+ "type": "object",
77
+ "description": "GraphQL query configuration",
78
+ "properties": {
79
+ "command": {
80
+ "description": "GraphQL document string"
81
+ },
82
+ "variables": {
83
+ "description": "Query variables (template expressions supported). Changing any referenced variable re-runs the query and rebuilds the board."
84
+ },
85
+ "path": {
86
+ "description": "Plain dot-path into the response (getObj; no [] segments here)"
87
+ }
88
+ }
89
+ },
90
+ "itemsPath": {
91
+ "type": "string",
92
+ "description": "Items sources only. Path relative to the query.path result, resolved with expandPath: every array-valued segment flat-maps automatically (no leading []; explicit [] segments are accepted no-ops). Missing/null segments yield []. Omitted -> the path result itself, normalized to an array.",
93
+ "x-example": "orderMoves"
94
+ },
95
+ "parentAs": {
96
+ "type": "string",
97
+ "description": "Items sources only. Attaches the direct owner of the final flattened collection to every item under this key (e.g. item.order).",
98
+ "x-example": "order"
99
+ },
100
+ "mapping": {
101
+ "type": "object",
102
+ "description": "Items sources only. Optional per-item template, context { ...variables, item } (item already carries the parent); result is merged over the raw item. Supply mapping.id when the entity has no id field."
103
+ }
104
+ },
105
+ "additionalProperties": true
106
+ },
107
+ "description": "All data sources, in the useComponentQueries convention. Every query NOT referenced by rows.fromQuery is an items source; items from all items sources concatenate. The pipeline runs once after all queries resolve."
108
+ },
109
+ "rows": {
110
+ "type": "object",
111
+ "required": [
112
+ "groupBy",
113
+ "headerWidth"
114
+ ],
115
+ "properties": {
116
+ "groupBy": {
117
+ "type": "string",
118
+ "description": "Dot-path ON the item -> row key (string-coerced). null/undefined/'' -> Unassigned.",
119
+ "x-example": "driver.contactId"
120
+ },
121
+ "fromQuery": {
122
+ "type": "string",
123
+ "description": "Name of the rows-source query. Rows come from it including rows with zero items. Omitted -> rows derived from distinct groupBy values in first-seen order."
124
+ },
125
+ "key": {
126
+ "type": "string",
127
+ "description": "Dot-path on the row record -> row key (string-coerced, must match groupBy values). Required with fromQuery.",
128
+ "x-example": "contactId"
129
+ },
130
+ "label": {
131
+ "description": "Row label template; context: item = the row record. Default: the row key.",
132
+ "x-example": "{{ item.firstName }} {{ item.lastName }}"
133
+ },
134
+ "orderBy": {
135
+ "type": "array",
136
+ "description": "Row sort over row fields (key, label, record.*). Default: rows-query order / first-seen order. Each entry is { field: dot-path to the sort value, direction: asc|desc (default asc) }. Multi-key and stable; null/undefined always sort last regardless of direction; ISO dates compare as dates, numbers numerically, strings locale-aware with numeric collation (natural sort, so numeric strings such as customValues sort by value)."
137
+ },
138
+ "unassigned": {
139
+ "type": "object",
140
+ "description": "Synthetic row collecting orphan items (group key matches no row). Always renders last.",
141
+ "properties": {
142
+ "show": {
143
+ "default": true,
144
+ "description": "false hides the row AND logs a warning with the dropped count — items are never silently dropped"
145
+ },
146
+ "label": {
147
+ "default": "Unassigned"
148
+ }
149
+ }
150
+ },
151
+ "headerWidth": {
152
+ "type": "number",
153
+ "description": "Row-header column width in px. Required: the column is sticky and virtualised rows need explicit widths to stay aligned."
154
+ },
155
+ "headerPadding": {
156
+ "default": 1,
157
+ "description": "Row-header cell padding in theme spacing units (1 = 4px), same idea as the top-level cellPadding but for the first column.",
158
+ "x-example": 2.5
159
+ }
160
+ },
161
+ "additionalProperties": true,
162
+ "description": "Row model: grouping key on the item plus an optional dedicated rows source (visible empty rows)"
163
+ },
164
+ "columns": {
165
+ "type": "object",
166
+ "required": [
167
+ "width"
168
+ ],
169
+ "properties": {
170
+ "orderBy": {
171
+ "type": "array",
172
+ "description": "Sort WITHIN each row -> slot order. Item at sorted index i renders in column i. Runs after ALL sources resolve. Each entry is { field: dot-path to the sort value, direction: asc|desc (default asc) }. Multi-key and stable; null/undefined always sort last regardless of direction; ISO dates compare as dates, numbers numerically, strings locale-aware with numeric collation (natural sort, so numeric strings such as customValues sort by value)."
173
+ },
174
+ "appendEmpty": {
175
+ "type": "boolean",
176
+ "description": "Always append one empty trailing slot: column count = largest group + 1 (1 when there are no items), so every lane keeps an empty cell for emptyCellTemplate / add-move affordances.",
177
+ "default": false
178
+ },
179
+ "label": {
180
+ "default": "{{ index }}",
181
+ "description": "Column header template; variables: index (1-based), columnIndex (0-based)",
182
+ "x-example": "Move {{ index }}"
183
+ },
184
+ "width": {
185
+ "type": "number",
186
+ "description": "Column width in px. Required: every virtualised row applies the same explicit gridTemplateColumns."
187
+ }
188
+ },
189
+ "additionalProperties": true,
190
+ "description": "Column model: ordinal slots. Column count = size of the largest group (+1 with appendEmpty); horizontal scroll, no cap."
191
+ },
192
+ "rowHeaderTemplate": {
193
+ "$ref": "../schemas.json#/definitions/component",
194
+ "description": "Row header cell; receives row { key, label, record, items, isUnassigned }, rowIndex, itemCount. Default: row.label as plain text."
195
+ },
196
+ "itemTemplate": {
197
+ "$ref": "../schemas.json#/definitions/component",
198
+ "description": "Item cell; receives item (incl. item.<parentAs> and intact nested collections — iterate them with `collection`), row, column { index, columnIndex, label }, rowIndex, columnIndex"
199
+ },
200
+ "emptyCellTemplate": {
201
+ "$ref": "../schemas.json#/definitions/component",
202
+ "description": "Optional; receives row, column, rowIndex, columnIndex. Default: empty cell."
203
+ },
204
+ "summaryComponent": {
205
+ "$ref": "../schemas.json#/definitions/component",
206
+ "description": "Optional per-column footer (sticky below the viewport); receives column, columnIndex, items (that column across all rows), totalCount"
207
+ },
208
+ "emptyStateTemplate": {
209
+ "$ref": "../schemas.json#/definitions/component",
210
+ "description": "Optional; shown when the pipeline yields zero items (rows from the rows query still render)"
211
+ },
212
+ "itemAccent": {
213
+ "description": "Optional per-item color template (same context as itemTemplate), usually a status color. Paints a bar down the card's left edge and a low-alpha wash of the same color over the card background. Accepts hex/rgb()/hsl() or a theme palette path ('warning.main'); anything unparseable renders the plain card.",
214
+ "x-example": "{{ item.orderMove.orderMoveStatus.color }}"
215
+ },
216
+ "itemGroups": {
217
+ "type": "object",
218
+ "required": [
219
+ "key"
220
+ ],
221
+ "properties": {
222
+ "key": {
223
+ "type": "string"
224
+ },
225
+ "headerTemplate": {
226
+ "$ref": "../schemas.json#/definitions/component"
227
+ },
228
+ "accent": {},
229
+ "dotsMenu": {
230
+ "type": "object"
231
+ }
232
+ },
233
+ "additionalProperties": false
234
+ },
235
+ "dragSource": {
236
+ "type": "object",
237
+ "required": [
238
+ "type"
239
+ ],
240
+ "properties": {
241
+ "type": {
242
+ "type": "string"
243
+ },
244
+ "data": {},
245
+ "canDrag": {}
246
+ },
247
+ "additionalProperties": false
248
+ },
249
+ "events": {
250
+ "type": "object",
251
+ "additionalProperties": {
252
+ "$ref": "../schemas.json#/definitions/actionsList"
253
+ },
254
+ "description": "Interaction handlers; payloads merge last over variables + store",
255
+ "properties": {
256
+ "onItemClick": {
257
+ "$ref": "../schemas.json#/definitions/actionsList",
258
+ "description": "Click on an item (propagation stopped); payload: item, row, column, rowIndex, columnIndex"
259
+ },
260
+ "onCellClick": {
261
+ "$ref": "../schemas.json#/definitions/actionsList",
262
+ "description": "Click on cell background — never fires from inside an item; payload: row, column, rowIndex, columnIndex"
263
+ },
264
+ "onRowClick": {
265
+ "$ref": "../schemas.json#/definitions/actionsList",
266
+ "description": "Click on the row header; payload: row, rowIndex"
267
+ },
268
+ "onItemsLoaded": {
269
+ "$ref": "../schemas.json#/definitions/actionsList",
270
+ "description": "Fires once per completed pipeline; payload: items, itemCount, rowCount, columnCount"
271
+ }
272
+ }
273
+ },
274
+ "cellPadding": {
275
+ "default": 0.5,
276
+ "description": "Gap between the card (or empty-cell content) and the cell separators, in theme spacing units (1 = 4px). Inner card padding is the item template's own business (options.p).",
277
+ "x-example": 2.5
278
+ }
279
+ },
280
+ "additionalProperties": true
281
+ },
282
+ "isHidden": {
283
+ "description": "Boolean template expression for conditional rendering"
284
+ }
285
+ },
286
+ "additionalProperties": true,
287
+ "description": "Generic grouped & ordered planning grid: each resource (driver, truck, dock, crew...) is a row, its work items appear as ordinal slots across the columns (slot 1, slot 2...). Columns are positional — the item's index in its sorted group — not dates; for date/time columns use timeline-grid. Fully YAML-driven four-stage pipeline: Extract (queries + itemsPath) -> Group (rows.groupBy) -> Sort (columns.orderBy) -> Place.",
288
+ "x-examples": [
289
+ {
290
+ "component": "planner",
291
+ "name": "driverPlanner",
292
+ "props": {
293
+ "height": 700,
294
+ "queries": [
295
+ {
296
+ "name": "moves",
297
+ "query": {
298
+ "command": "query GetOrderMovesForPlanner($date: DateTime!, $branchId: Int) { orders(filter: { orderMoves: { date: $date } }) { items { orderId orderNumber orderMoves { orderMoveId startDate driver { contactId firstName lastName } orderMoveLegs { legType location } } } } }",
299
+ "variables": {
300
+ "date": "{{ plannerDate }}",
301
+ "branchId": "{{ branchId }}"
302
+ },
303
+ "path": "orders.items"
304
+ },
305
+ "itemsPath": "orderMoves",
306
+ "parentAs": "order",
307
+ "mapping": {
308
+ "id": "{{ item.orderMoveId }}"
309
+ }
310
+ },
311
+ {
312
+ "name": "drivers",
313
+ "query": {
314
+ "command": "query GetPlannerDrivers($branchId: Int) { contacts(filter: { branchId: $branchId }) { items { contactId firstName lastName } } }",
315
+ "variables": {
316
+ "branchId": "{{ branchId }}"
317
+ },
318
+ "path": "contacts.items"
319
+ }
320
+ }
321
+ ],
322
+ "rows": {
323
+ "groupBy": "driver.contactId",
324
+ "fromQuery": "drivers",
325
+ "key": "contactId",
326
+ "label": "{{ item.firstName }} {{ item.lastName }}",
327
+ "orderBy": [
328
+ {
329
+ "field": "label",
330
+ "direction": "asc"
331
+ }
332
+ ],
333
+ "unassigned": {
334
+ "show": true,
335
+ "label": "Unassigned"
336
+ },
337
+ "headerWidth": 260
338
+ },
339
+ "columns": {
340
+ "orderBy": [
341
+ {
342
+ "field": "startDate",
343
+ "direction": "asc"
344
+ },
345
+ {
346
+ "field": "orderMoveId",
347
+ "direction": "asc"
348
+ }
349
+ ],
350
+ "label": "Move {{ index }}",
351
+ "width": 420
352
+ },
353
+ "itemTemplate": {
354
+ "component": "card",
355
+ "props": {
356
+ "title": "{{ item.order.orderNumber }}"
357
+ }
358
+ },
359
+ "events": {
360
+ "onItemClick": [
361
+ {
362
+ "navigate": "orders/{{ item.order.orderId }}"
363
+ }
364
+ ]
365
+ }
366
+ }
367
+ }
368
+ ]
369
+ }
@@ -1 +1,115 @@
1
- {"$schema":"http://json-schema.org/draft-07/schema#","title":"ProgressBar Component","type":"object","required":["component"],"properties":{"component":{"const":"progressBar"},"name":{"type":"string"},"props":{"type":"object","properties":{"value":{},"max":{},"items":{},"completedPath":{"type":"string"},"completedValue":{},"label":{},"showValue":{"type":"boolean"},"valueFormat":{"enum":["count","percent"]},"variant":{"enum":["linear","circular","segmented"]},"height":{},"size":{},"color":{},"activeColor":{"type":"string","description":"Mobile segmented variant color for completed segments"},"trackColor":{"type":"string","description":"Track or incomplete-segment color"},"tooltip":{},"testID":{"type":"string"}},"additionalProperties":true}},"additionalProperties":true}
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "ProgressBar Component",
4
+ "type": "object",
5
+ "required": [
6
+ "component"
7
+ ],
8
+ "properties": {
9
+ "component": {
10
+ "const": "progressBar"
11
+ },
12
+ "name": {
13
+ "type": "string",
14
+ "description": "Unique name identifier for the progress bar"
15
+ },
16
+ "props": {
17
+ "type": "object",
18
+ "properties": {
19
+ "value": {
20
+ "description": "Completed amount. Overrides the derived count when items/completedPath are also set"
21
+ },
22
+ "max": {
23
+ "description": "Total amount. Defaults to items.length when items is set. max 0 or missing → nothing renders"
24
+ },
25
+ "items": {
26
+ "description": "Single-chunk template resolving to an array, e.g. '{{ row.items }}'. Sets max = length; combined with completedPath sets value = matched count"
27
+ },
28
+ "completedPath": {
29
+ "type": "string",
30
+ "description": "Dot path inside each item to test, e.g. orderMove.orderMoveStatus.statusStage"
31
+ },
32
+ "completedValue": {
33
+ "description": "Value at completedPath that counts as completed, e.g. Completed. Omitted → any truthy value counts"
34
+ },
35
+ "label": {
36
+ "description": "Caption on the left above the bar, e.g. Moves"
37
+ },
38
+ "showValue": {
39
+ "type": "boolean",
40
+ "description": "Show the value caption (right-aligned for linear, centered for circular). Default true"
41
+ },
42
+ "valueFormat": {
43
+ "enum": [
44
+ "count",
45
+ "percent"
46
+ ],
47
+ "description": "count → '2/5' (default), percent → '40%'"
48
+ },
49
+ "variant": {
50
+ "enum": [
51
+ "linear",
52
+ "circular",
53
+ "segmented"
54
+ ],
55
+ "description": "Bar shape: linear (default), circular or segmented"
56
+ },
57
+ "height": {
58
+ "description": "Linear bar thickness in px. Default 6"
59
+ },
60
+ "size": {
61
+ "description": "Circular variant diameter in px. Default 40"
62
+ },
63
+ "color": {
64
+ "description": "Theme color (primary, success, warning, info, error, secondary) or CSS color. Default: primary, flipping to success at 100%"
65
+ },
66
+ "activeColor": {
67
+ "type": "string",
68
+ "description": "Mobile segmented variant color for completed segments"
69
+ },
70
+ "trackColor": {
71
+ "type": "string",
72
+ "description": "Track or incomplete-segment color"
73
+ },
74
+ "tooltip": {
75
+ "description": "Tooltip on hover"
76
+ },
77
+ "testID": {
78
+ "type": "string"
79
+ },
80
+ "isHidden": {
81
+ "description": "Conditionally hide the bar"
82
+ },
83
+ "permission": {
84
+ "description": "Permission(s) required to render the component: a permission string or an array of permission strings"
85
+ }
86
+ },
87
+ "additionalProperties": true
88
+ }
89
+ },
90
+ "additionalProperties": true,
91
+ "description": "Abstract progress bar. Feed it value/max directly, or an items array with a declarative completed predicate — what the numbers mean (moves, stops, hours…) is the module's business. Renders nothing when max is 0, so it can be declared unconditionally.",
92
+ "x-examples": [
93
+ {
94
+ "component": "progressBar",
95
+ "name": "laneMovesProgress",
96
+ "props": {
97
+ "label": "Moves",
98
+ "items": "{{ row.items }}",
99
+ "completedPath": "orderMove.orderMoveStatus.statusStage",
100
+ "completedValue": "Completed"
101
+ }
102
+ },
103
+ {
104
+ "component": "progressBar",
105
+ "name": "hoursOfService",
106
+ "props": {
107
+ "label": "Hours",
108
+ "value": "{{ row.record.customValues.hoursUsed }}",
109
+ "max": 11,
110
+ "valueFormat": "percent",
111
+ "color": "warning"
112
+ }
113
+ }
114
+ ]
115
+ }
@@ -115,6 +115,30 @@
115
115
  "$ref": "../schemas.json#/definitions/component",
116
116
  "description": "Mobile-only component used for normal-mode events whose mapped status is 'active'; receives 'item' bound to the event"
117
117
  },
118
+ "itemGroups": {
119
+ "type": "object",
120
+ "description": "Mobile normal-mode visual grouping for adjacent events whose templated key resolves to the same non-empty value",
121
+ "properties": {
122
+ "key": {
123
+ "$ref": "../schemas.json#/definitions/templateExpression",
124
+ "description": "Resolved per event with item, index, and collection"
125
+ },
126
+ "accent": {
127
+ "$ref": "../schemas.json#/definitions/templateExpression",
128
+ "description": "Frame color resolved with group and collection"
129
+ },
130
+ "background": {
131
+ "$ref": "../schemas.json#/definitions/templateExpression",
132
+ "description": "Frame fill resolved with group and collection"
133
+ },
134
+ "headerTemplate": {
135
+ "$ref": "../schemas.json#/definitions/component",
136
+ "description": "Rendered once above the grouped members; receives group and collection"
137
+ }
138
+ },
139
+ "required": ["key"],
140
+ "additionalProperties": false
141
+ },
118
142
  "eventSources": {
119
143
  "type": "array",
120
144
  "description": "Data providers executed for the current visible range or tracking milestone source",
@@ -84,13 +84,37 @@
84
84
  "items": { "type": "string" }
85
85
  },
86
86
  "parentId": {
87
- "oneOf": [{ "type": "string" }, { "type": "integer" }, { "$ref": "../schemas.json#/definitions/templateExpression" }],
87
+ "anyOf": [{ "type": "string" }, { "type": "integer" }, { "$ref": "../schemas.json#/definitions/templateExpression" }],
88
88
  "description": "Parent entity ID for linking attachment"
89
89
  },
90
90
  "parentType": {
91
- "oneOf": [{ "type": "string" }, { "$ref": "../schemas.json#/definitions/templateExpression" }],
91
+ "anyOf": [{ "type": "string" }, { "$ref": "../schemas.json#/definitions/templateExpression" }],
92
92
  "description": "Logical parent type/entity for the attachment (e.g., Order, Contact)"
93
93
  },
94
+ "links": {
95
+ "oneOf": [
96
+ {
97
+ "type": "array",
98
+ "items": {
99
+ "type": "object",
100
+ "properties": {
101
+ "entityType": {
102
+ "type": "string",
103
+ "description": "Linked entity type: Order, Contact, Job or TrackingEvent"
104
+ },
105
+ "entityId": {
106
+ "type": ["string", "integer"],
107
+ "description": "Linked entity id (int, or UUID for jobs); templates allowed"
108
+ }
109
+ },
110
+ "required": ["entityType", "entityId"],
111
+ "additionalProperties": false
112
+ }
113
+ },
114
+ { "$ref": "../schemas.json#/definitions/templateExpression" }
115
+ ],
116
+ "description": "More entities to link the uploaded attachment to, besides parentId/parentType. Entries whose entityType or entityId resolves empty are skipped."
117
+ },
94
118
  "onUploaded": {
95
119
  "oneOf": [{ "type": "string" }, { "$ref": "../schemas.json#/definitions/actions" }],
96
120
  "description": "Actions to execute after successful upload"
@@ -151,6 +175,15 @@
151
175
  "parentId": "{{ orderId }}",
152
176
  "parentType": "Order"
153
177
  }
178
+ },
179
+ {
180
+ "type": "attachment",
181
+ "label": { "en-US": "Event Photo" },
182
+ "options": {
183
+ "parentId": "{{ orderId }}",
184
+ "parentType": "Order",
185
+ "links": [{ "entityType": "TrackingEvent", "entityId": "{{ trackingEventId }}" }]
186
+ }
154
187
  }
155
188
  ]
156
189
  }