@jigx/core-sdk 1.1.0 → 1.2.0-rc
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.
- package/dist/action/ja.generate-pdf.d.ts +17 -1
- package/dist/action/ja.generate-pdf.d.ts.map +1 -1
- package/dist/action/ja.generate-pdf.js +4 -1
- package/dist/action/ja.in-background.d.ts +3 -2
- package/dist/action/ja.in-background.d.ts.map +1 -1
- package/dist/action/ja.in-background.js +1 -1
- package/dist/assets/example-extraction-cache.json +3 -3
- package/dist/assets/extracted-core-sdk-examples.yaml +18 -0
- package/dist/assets/extracted-core-sdk-types.yaml +58 -0
- package/dist/assets/type-extraction-cache.json +3 -3
- package/docs/array-fields.md +371 -0
- package/docs/conditional-logic.md +178 -0
- package/docs/convention-naming.md +102 -0
- package/docs/date-field.md +92 -0
- package/docs/dropdown-fields.md +879 -0
- package/docs/field-state.md +131 -0
- package/docs/field-types-overview.md +132 -0
- package/docs/formatting.md +421 -0
- package/docs/icons.md +142 -0
- package/docs/index.md +23 -0
- package/docs/jsonata-expressions.md +200 -0
- package/docs/media-fields.md +107 -0
- package/docs/overview.md +467 -0
- package/docs/pattern-build-deploy.md +91 -0
- package/docs/pattern-datasources.md +459 -0
- package/docs/pattern-forms.md +528 -0
- package/docs/pattern-global-actions.md +92 -0
- package/docs/pattern-javascript-functions.md +452 -0
- package/docs/pattern-navigation.md +304 -0
- package/docs/pattern-pdf-generation.md +391 -0
- package/docs/pattern-rest-acumatica.md +660 -0
- package/docs/pattern-sync-progress.md +96 -0
- package/docs/pattern-sync.md +653 -0
- package/docs/pattern-tabs-form.md +293 -0
- package/docs/recipe-index.md +64 -0
- package/docs/runtime-variables.md +127 -0
- package/docs/sections.md +81 -0
- package/docs/validation-patterns.md +150 -0
- package/package.json +3 -2
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
## Static Options
|
|
2
|
+
|
|
3
|
+
Hardcode options directly:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
step.addDropdown({
|
|
7
|
+
name: 'priority',
|
|
8
|
+
label: 'Priority Level',
|
|
9
|
+
data: [
|
|
10
|
+
{ label: 'High', value: 'high', icon: 'alert-triangle' },
|
|
11
|
+
{ label: 'Medium', value: 'medium', icon: 'alert-circle' },
|
|
12
|
+
{ label: 'Low', value: 'low', icon: 'information-circle' },
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Static options support one visual element: `icon`, `image`, or `avatar`.
|
|
18
|
+
|
|
19
|
+
## Icons in Options
|
|
20
|
+
|
|
21
|
+
Both dropdown and choice-field support icons using icon name strings:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const options = [
|
|
25
|
+
{ label: 'High', value: 'high', icon: 'alert-triangle' },
|
|
26
|
+
{ label: 'Low', value: 'low', icon: 'information-circle' }
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Icon names are validated at build time. Use `mcp__jigx_common__search_sdk_icons` to find valid names.
|
|
31
|
+
|
|
32
|
+
## Dynamic Datasource
|
|
33
|
+
|
|
34
|
+
For database-backed data, create a datasource then reference it:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Create datasource
|
|
38
|
+
const categoriesData = step
|
|
39
|
+
.addDatasource({ provider: 'dynamic', datasourceId: 'categories-ds' })
|
|
40
|
+
.table('default/categories')
|
|
41
|
+
.query(
|
|
42
|
+
"SELECT id AS value, json_extract(data, '$.label') AS label FROM [default/categories]",
|
|
43
|
+
)
|
|
44
|
+
// Reference in dropdown
|
|
45
|
+
step.addDropdown({
|
|
46
|
+
name: 'category',
|
|
47
|
+
label: 'Category',
|
|
48
|
+
data: categoriesData,
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Provider options:**
|
|
53
|
+
|
|
54
|
+
- `'dynamic'` - Cloud-synced, offline-first, auto-registered (recommended)
|
|
55
|
+
- `'local'` - Device-only storage
|
|
56
|
+
|
|
57
|
+
**Required columns:**
|
|
58
|
+
|
|
59
|
+
- `id AS value` - identifier
|
|
60
|
+
- `json_extract(data, '$.label') AS label` - display text
|
|
61
|
+
- Optional: `json_extract(data, '$.icon') AS icon`
|
|
62
|
+
|
|
63
|
+
**Document schema:** Tables have `id` and `data` columns. Use `json_extract(data, '$.field')` to access fields.
|
|
64
|
+
|
|
65
|
+
### Value matching rule
|
|
66
|
+
|
|
67
|
+
For datasource-backed dropdowns, the dropdown `value` must come from a concrete top-level field on the datasource row so Jigx can match the selected value back to the datasource item.
|
|
68
|
+
|
|
69
|
+
Preferred pattern:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
screen.addDatasource
|
|
73
|
+
.sqlite({ datasourceId: 'customers', provider: 'local' })
|
|
74
|
+
.entity('Customers')
|
|
75
|
+
.query(`
|
|
76
|
+
SELECT id,
|
|
77
|
+
data,
|
|
78
|
+
json_extract(data, '$.CustomerID.value') as CustomerID,
|
|
79
|
+
json_extract(data, '$.CustomerName.value') as CustomerName
|
|
80
|
+
FROM [Customers]
|
|
81
|
+
`)
|
|
82
|
+
.jsonProperties('data')
|
|
83
|
+
|
|
84
|
+
const customerDropdown = form.addControl.dropdown({
|
|
85
|
+
instanceId: 'Customer',
|
|
86
|
+
label: 'Customer',
|
|
87
|
+
data: '=@ctx.datasources.customers',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
customerDropdown.addControl
|
|
91
|
+
.item()
|
|
92
|
+
.title('=@ctx.current.item.CustomerID')
|
|
93
|
+
.description('=@ctx.current.item.CustomerName')
|
|
94
|
+
.value('=@ctx.current.item.CustomerID')
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Do not rely on nested `data.*` fields alone for dropdown matching when a stable selected value is required.
|
|
98
|
+
|
|
99
|
+
### Sort by key, describe with name
|
|
100
|
+
|
|
101
|
+
For Acumatica lookup dropdowns, sort by the business key users actually recognize first:
|
|
102
|
+
|
|
103
|
+
- `CustomerID`
|
|
104
|
+
- `LocationID`
|
|
105
|
+
- `ProjectID`
|
|
106
|
+
- `InventoryID`
|
|
107
|
+
- `WarehouseID`
|
|
108
|
+
- similar code/key fields
|
|
109
|
+
|
|
110
|
+
Then show the human-readable label in `description(...)` when it exists.
|
|
111
|
+
|
|
112
|
+
Preferred pattern:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
.query(`
|
|
116
|
+
SELECT id,
|
|
117
|
+
data,
|
|
118
|
+
json_extract(data, '$.LocationID.value') as LocationID,
|
|
119
|
+
json_extract(data, '$.LocationName.value') as LocationName
|
|
120
|
+
FROM [CustomerLocations]
|
|
121
|
+
ORDER BY json_extract(data, '$.LocationID.value')
|
|
122
|
+
`)
|
|
123
|
+
|
|
124
|
+
locationDropdown.addControl
|
|
125
|
+
.item()
|
|
126
|
+
.title('=@ctx.current.item.LocationID')
|
|
127
|
+
.description('=@ctx.current.item.LocationName')
|
|
128
|
+
.value('=@ctx.current.item.LocationID')
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Valid-combination matrix dropdowns
|
|
132
|
+
|
|
133
|
+
When dropdown choices must be constrained by earlier selections, use a compact support table with one row per valid terminal item/configuration. Store stable top-level fields for every selectable attribute and the final external key.
|
|
134
|
+
|
|
135
|
+
For small or manually maintained matrices, Dynamic Data is fine. For large matrices, prefer a REST-backed initial sync into a local SQLite table. Keep the row shape identical between the REST response and the Dynamic Data shape so the app can switch sources without rewriting dropdown queries.
|
|
136
|
+
|
|
137
|
+
Recommended support-table shape:
|
|
138
|
+
|
|
139
|
+
- `id`: deterministic row id, short enough for Dynamic Data.
|
|
140
|
+
- `chainId`: manufacturer/configuration chain, for example `AMARR`, `CHI`, `CLOPAY`, `WAYNEDALTON`.
|
|
141
|
+
- Optional external key fields such as `InventoryID`, if the app needs the exact external record id at runtime. If the app can construct the output code from validated selections, omit external keys to keep the support table smaller.
|
|
142
|
+
- Attribute columns using the exact Acumatica attribute IDs and case, for example `SIZE`, `MODSER`, `COLORAM`, `PANELDES`, `GLASSTYPE`, `WNDOWOPTAM`, `TRACK`, `SPR`.
|
|
143
|
+
- Optional metadata such as `Description`, `ItemClass`, `ItemStatus`, `selectionKey`.
|
|
144
|
+
|
|
145
|
+
For a Dynamic Data matrix, register the support table in `database.ts` and add an app-scoped dynamic datasource so local SQL datasources can join/query it offline:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// common.ts
|
|
149
|
+
DOOR_MATRIX_ITEMS: { TABLE: 'doorMatrixItems', FQN: 'default/doorMatrixItems' }
|
|
150
|
+
|
|
151
|
+
// database.ts
|
|
152
|
+
app.addDatabase.default.table(ENTITY.DOOR_MATRIX_ITEMS.TABLE)
|
|
153
|
+
|
|
154
|
+
// datasources.ts
|
|
155
|
+
app.addDatasource
|
|
156
|
+
.sqlite({ datasourceId: 'data-register-door-matrix-items', provider: 'dynamic' })
|
|
157
|
+
.entity(ENTITY.DOOR_MATRIX_ITEMS.FQN)
|
|
158
|
+
.query(`SELECT id, data FROM [${ENTITY.DOOR_MATRIX_ITEMS.FQN}]`)
|
|
159
|
+
.jsonProperties('data')
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For large support tables, seed Dynamic Data through the Data API bulk merge endpoint instead of one row at a time. The accepted request is `PATCH /v2.0/data/organizations/{organizationId}/solutions/{solutionId}/databases/default/tables/{tableId}/rows` with a body keyed by row id, where each value wraps the row content:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"1": {
|
|
167
|
+
"content": {
|
|
168
|
+
"chainId": "AMARR",
|
|
169
|
+
"v1": "10X10",
|
|
170
|
+
"v2": "HE3000"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Keep batches under the API validation limit. In practice, batches of 250 rows are a safe default for large matrix imports. The row id should stay outside `content`; duplicate imports are safe because the endpoint merges by row id.
|
|
177
|
+
|
|
178
|
+
For a REST-backed matrix, define the matrix entity as a local table, expose an endpoint with `$top` and `$skip`, and use a REST function with continuation:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
provider: DATA_PROVIDER_REST
|
|
182
|
+
method: GET
|
|
183
|
+
url: https://{matrixApiURL}/matrix/doorMatrixItems
|
|
184
|
+
useLocalCall: true
|
|
185
|
+
|
|
186
|
+
parameters:
|
|
187
|
+
accessToken:
|
|
188
|
+
location: header
|
|
189
|
+
type: jigx
|
|
190
|
+
value: jigx
|
|
191
|
+
required: true
|
|
192
|
+
matrixApiURL:
|
|
193
|
+
location: path
|
|
194
|
+
type: string
|
|
195
|
+
required: true
|
|
196
|
+
$top:
|
|
197
|
+
location: query
|
|
198
|
+
type: number
|
|
199
|
+
value: 5000
|
|
200
|
+
required: true
|
|
201
|
+
$skip:
|
|
202
|
+
location: query
|
|
203
|
+
type: number
|
|
204
|
+
value: 0
|
|
205
|
+
required: true
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The backend should validate the Jigx token before returning data, then return rows in the same shape the app expects locally:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"top": 5000,
|
|
213
|
+
"skip": 0,
|
|
214
|
+
"value": [
|
|
215
|
+
{
|
|
216
|
+
"id": "1",
|
|
217
|
+
"chainId": "AMARR",
|
|
218
|
+
"v1": "10X10",
|
|
219
|
+
"v2": "HE3000",
|
|
220
|
+
"createdDateAndTime": "2026-05-09T12:00:00.000Z"
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use a `syncEntities().rest().entityWithFunction(...)` action during initial sync to save those records into the local table. Do not prefix the local table with `default/`; query it as `[doorMatrixItems]`.
|
|
227
|
+
|
|
228
|
+
For each dependent dropdown, return normal attribute values when strict validation is off and matrix-filtered values when strict validation is on. The selected value still needs to be a concrete top-level field returned by the datasource, usually `valueID`.
|
|
229
|
+
|
|
230
|
+
If source attribute values are temporarily wrong but the generated code must use corrected values, do not hard-code corrections into every dropdown or save action. Add a small Dynamic Data override table, for example `default/attributeValueOverrides`, with `attributeID`, `sourceValueID`, and `outputValueID`. If an attribute is shared by multiple manufacturers or chains, add a scope field such as `chainId` and apply overrides only for that chain; otherwise a correction for one manufacturer can silently change another manufacturer's generated code. Join or scalar-lookup that table inside the dropdown datasource so both unrestricted AttributeDetails rows and strict matrix rows expose the same corrected `valueID`. Apply the same corrected expression in every matrix filter and final matrix validation query, otherwise downstream dropdowns will compare corrected saved values to uncorrected matrix rows and return no results.
|
|
231
|
+
|
|
232
|
+
```sql
|
|
233
|
+
SELECT id, valueID, description, sortOrder
|
|
234
|
+
FROM (
|
|
235
|
+
SELECT id,
|
|
236
|
+
json_extract(data, '$.valueID') as valueID,
|
|
237
|
+
json_extract(data, '$.description') as description,
|
|
238
|
+
json_extract(data, '$.sortOrder') as sortOrder
|
|
239
|
+
FROM [AttributeDetails]
|
|
240
|
+
WHERE @strict != '1'
|
|
241
|
+
AND json_extract(data, '$.attributeID') = 'MODSER'
|
|
242
|
+
|
|
243
|
+
UNION ALL
|
|
244
|
+
|
|
245
|
+
SELECT 'MODSER:' || option.valueID as id,
|
|
246
|
+
option.valueID as valueID,
|
|
247
|
+
COALESCE(NULLIF(json_extract(ad.data, '$.description'), ''), option.valueID) as description,
|
|
248
|
+
json_extract(ad.data, '$.sortOrder') as sortOrder
|
|
249
|
+
FROM (
|
|
250
|
+
SELECT DISTINCT json_extract(m.data, '$.MODSER') as valueID
|
|
251
|
+
FROM [doorMatrixItems] m
|
|
252
|
+
WHERE @strict = '1'
|
|
253
|
+
AND json_extract(m.data, '$.chainId') = @chainId
|
|
254
|
+
AND json_extract(m.data, '$.SIZE') = @size
|
|
255
|
+
AND COALESCE(NULLIF(json_extract(m.data, '$.MODSER'), ''), '') != ''
|
|
256
|
+
) option
|
|
257
|
+
LEFT JOIN [AttributeDetails] ad
|
|
258
|
+
ON json_extract(ad.data, '$.attributeID') = 'MODSER'
|
|
259
|
+
AND json_extract(ad.data, '$.valueID') = option.valueID
|
|
260
|
+
)
|
|
261
|
+
ORDER BY LOWER(COALESCE(NULLIF(description, ''), valueID)), LOWER(valueID)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When strict validation is enabled, each dropdown should filter its datasource by the earlier selected fields. Disable the save action unless a terminal matrix datasource confirms that the completed selection has at least one valid match. If the output code is truly deterministic from the exact selected code segments, construct it from the selected values instead of storing every external id in the support table.
|
|
265
|
+
|
|
266
|
+
If the external identifier can include fixed manufacturer prefixes, omitted segments, legacy aliases, corrected segment values, or any other value that is not exactly represented by the visible dropdown selections, include the resolved external identifier on the terminal matrix row and use that value for strict-mode save. Dropdown overrides can make display and filtering values consistent, but the final strict-mode payload should still use the matrix-resolved identifier to avoid drifting from the authoritative external system.
|
|
267
|
+
|
|
268
|
+
Fields that are optional in unrestricted mode may still be required in strict mode when the matrix needs them to identify a valid combination. In that case, drive `isRequired` and downstream disable rules from the strict-mode flag, not from a hard-coded required setting.
|
|
269
|
+
|
|
270
|
+
Large valid-combination tables should create SQLite expression indexes after the initial data sync. Use an `action.execute-sql` step at the end of the global initial-sync action and index the JSON fields used by matrix dropdown filters, for example `(json_extract(data, '$.chainId'), json_extract(data, '$.v1'), json_extract(data, '$.v2'))`. Also index lookup tables by `(attributeID, valueID)` and any override table by `(attributeID, sourceValueID)`. Keep the index action idempotent with `CREATE INDEX IF NOT EXISTS`.
|
|
271
|
+
|
|
272
|
+
### Datasource Reference Methods
|
|
273
|
+
|
|
274
|
+
Use `ds.ref` and `ds.getFieldRef()` to generate runtime references for datasource fields. Works for any field that needs datasource values - not just dropdowns:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const employeesDs = step
|
|
278
|
+
.addDatasource({ provider: 'dynamic', datasourceId: 'employees-ds' })
|
|
279
|
+
.table('default/employees')
|
|
280
|
+
.query(
|
|
281
|
+
"SELECT id AS value, json_extract(data, '$.name') AS label, json_extract(data, '$.email') AS email FROM [default/employees]",
|
|
282
|
+
)
|
|
283
|
+
// ds.ref returns base datasource reference: '@ctx.datasources.employees-ds'
|
|
284
|
+
// ds.getFieldRef('email') returns field reference: '@ctx.datasources.employees-ds.email'
|
|
285
|
+
capturedRef = employeesDs.ref
|
|
286
|
+
capturedFieldRef = employeesDs.getFieldRef('email')
|
|
287
|
+
step.addDropdown({
|
|
288
|
+
name: 'employee',
|
|
289
|
+
label: 'Employee',
|
|
290
|
+
data: employeesDs,
|
|
291
|
+
})
|
|
292
|
+
// Use getFieldRef to reference datasource fields in expressions
|
|
293
|
+
step.addText({
|
|
294
|
+
name: 'emailDisplay',
|
|
295
|
+
label: 'Selected Email',
|
|
296
|
+
value: new JsonataBuilder('$email', {
|
|
297
|
+
email: employeesDs.getFieldRef('email'),
|
|
298
|
+
}),
|
|
299
|
+
isDisabled: true,
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
| Method | Use Case | Returns |
|
|
304
|
+
| --- | --- | --- |
|
|
305
|
+
| `ds.ref` | Reference entire datasource row | `@ctx.datasources.my-ds` |
|
|
306
|
+
| `ds.getFieldRef('field')` | Reference specific field | `@ctx.datasources.my-ds.field` |
|
|
307
|
+
| `ds.getFieldRef('a.b')` | Nested field paths | `@ctx.datasources.my-ds.a.b` |
|
|
308
|
+
|
|
309
|
+
**Prefilling multiple fields from datasource:**
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// CORRECT: Use ds.ref for entire row, access fields in Jsonata
|
|
313
|
+
step.addText({
|
|
314
|
+
name: 'firstName',
|
|
315
|
+
label: 'First Name',
|
|
316
|
+
value: new JsonataBuilder('$data.firstName', { data: contactDs.ref }),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// WRONG: Do NOT use getFieldRef with empty string
|
|
320
|
+
// value: new JsonataBuilder('$data.firstName', { data: contactDs.getFieldRef('') })
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Ref:** `./external-data-packages.md` for full employee contact prefill example
|
|
324
|
+
|
|
325
|
+
## Searchable Dropdowns
|
|
326
|
+
|
|
327
|
+
Enable real-time filtering as users type. **Searchable dropdowns require 3 steps** — setting `isSearchable: true` alone only shows a search bar UI, it does NOT filter. You MUST also: (1) add `@searchText IS NULL OR @searchText = '' OR ... LIKE '%' || @searchText || '%'` to the SQL WHERE clause, and (2) call `datasource.addQueryParameter('searchText', new JsonataBuilder('$s', { s: dropdown.state.searchText }))` after creating the dropdown.
|
|
328
|
+
|
|
329
|
+
**When to use:**
|
|
330
|
+
|
|
331
|
+
- Large datasets (100+ items)
|
|
332
|
+
- User needs to find specific items quickly
|
|
333
|
+
- Remote/synced data where client-side filtering is insufficient
|
|
334
|
+
|
|
335
|
+
### Basic Pattern
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const vendorData = step
|
|
339
|
+
.addDatasource({ provider: 'dynamic', datasourceId: 'vendor-datasource' })
|
|
340
|
+
.table('default/vendors').query(`
|
|
341
|
+
SELECT
|
|
342
|
+
id AS value,
|
|
343
|
+
json_extract(data, '$.VendorName') AS label
|
|
344
|
+
FROM [default/vendors]
|
|
345
|
+
WHERE @searchText IS NULL OR @searchText = ''
|
|
346
|
+
OR json_extract(data, '$.VendorName') LIKE '%' || @searchText || '%'
|
|
347
|
+
OR json_extract(data, '$.VendorCode') LIKE '%' || @searchText || '%'
|
|
348
|
+
`)
|
|
349
|
+
// Create dropdown with isSearchable, then bind its searchText state to query parameter
|
|
350
|
+
const vendor = step.addDropdown({
|
|
351
|
+
name: 'vendor',
|
|
352
|
+
label: 'Select Vendor',
|
|
353
|
+
data: vendorData,
|
|
354
|
+
isSearchable: true,
|
|
355
|
+
})
|
|
356
|
+
// Use dropdown.state.searchText to get the expression
|
|
357
|
+
vendorData.addQueryParameter(
|
|
358
|
+
'searchText',
|
|
359
|
+
new JsonataBuilder('$s', { s: vendor.state.searchText }),
|
|
360
|
+
)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Key points:**
|
|
364
|
+
|
|
365
|
+
- Set `isSearchable: true` on the dropdown
|
|
366
|
+
- SQL query uses `@searchText` parameter with NULL/empty check
|
|
367
|
+
- Create dropdown first, then bind `dropdown.state.searchText` to query parameter
|
|
368
|
+
- Use `LIKE '%' || @searchText || '%'` for partial matching
|
|
369
|
+
- Search multiple fields with `OR` conditions
|
|
370
|
+
|
|
371
|
+
### Choosing Search Fields
|
|
372
|
+
|
|
373
|
+
Search across 4-5 fields for best user experience. Always include basic identifiers (id, name/label), then add fields users might search by — email, phone, city, code, etc.
|
|
374
|
+
|
|
375
|
+
Pick fields based on the datasource schema. For example, a Customers table might search:
|
|
376
|
+
|
|
377
|
+
```sql
|
|
378
|
+
WHERE @searchText IS NULL OR @searchText = ''
|
|
379
|
+
OR json_extract(data, '$.CustomerName.value') LIKE '%' || @searchText || '%'
|
|
380
|
+
OR json_extract(data, '$.CustomerID.value') LIKE '%' || @searchText || '%'
|
|
381
|
+
OR json_extract(data, '$.Email.value') LIKE '%' || @searchText || '%'
|
|
382
|
+
OR json_extract(data, '$.City.value') LIKE '%' || @searchText || '%'
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Guidelines:**
|
|
386
|
+
|
|
387
|
+
- Default: always search `id` / `name` (or equivalent label field)
|
|
388
|
+
- Ask the user which additional fields to include if not specified in prompt
|
|
389
|
+
- Verify field paths exist in the datasource schema before adding to WHERE clause
|
|
390
|
+
- More search fields = better discoverability, minimal performance impact on SQLite
|
|
391
|
+
|
|
392
|
+
### Multiple Filters with searchText
|
|
393
|
+
|
|
394
|
+
Combine searchText with additional filter parameters:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// Category filter (static dropdown)
|
|
398
|
+
const category = step.addDropdown({
|
|
399
|
+
name: 'category',
|
|
400
|
+
label: 'Category',
|
|
401
|
+
data: [
|
|
402
|
+
{ label: 'Electronics', value: 'electronics' },
|
|
403
|
+
{ label: 'Clothing', value: 'clothing' },
|
|
404
|
+
{ label: 'All', value: '' },
|
|
405
|
+
],
|
|
406
|
+
})
|
|
407
|
+
const productData = step
|
|
408
|
+
.addDatasource({ provider: 'dynamic', datasourceId: 'product-datasource' })
|
|
409
|
+
.table('default/products').query(`
|
|
410
|
+
SELECT
|
|
411
|
+
id AS value,
|
|
412
|
+
json_extract(data, '$.name') AS label
|
|
413
|
+
FROM [default/products]
|
|
414
|
+
WHERE (@searchText IS NULL OR @searchText = ''
|
|
415
|
+
OR json_extract(data, '$.name') LIKE '%' || @searchText || '%')
|
|
416
|
+
AND (@category IS NULL OR @category = ''
|
|
417
|
+
OR json_extract(data, '$.category') = @category)
|
|
418
|
+
`)
|
|
419
|
+
// Create dropdown with isSearchable, then bind parameters
|
|
420
|
+
const product = step.addDropdown({
|
|
421
|
+
name: 'product',
|
|
422
|
+
label: 'Search Products',
|
|
423
|
+
data: productData,
|
|
424
|
+
isSearchable: true,
|
|
425
|
+
})
|
|
426
|
+
// Bind searchText using dropdown.state.searchText
|
|
427
|
+
productData.addQueryParameter(
|
|
428
|
+
'searchText',
|
|
429
|
+
new JsonataBuilder('$s', { s: product.state.searchText }),
|
|
430
|
+
)
|
|
431
|
+
// Bind category filter parameter using JsonataBuilder
|
|
432
|
+
productData.addQueryParameter(
|
|
433
|
+
'category',
|
|
434
|
+
new JsonataBuilder('$cat', { cat: category.state.value }),
|
|
435
|
+
)
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Filter parameter patterns:**
|
|
439
|
+
|
|
440
|
+
- searchText: Use `dropdown.state.searchText` property
|
|
441
|
+
- Other filters: Use `JsonataBuilder` for field references
|
|
442
|
+
|
|
443
|
+
### SQL Query Pattern
|
|
444
|
+
|
|
445
|
+
Always include NULL/empty checks to show all results when search is empty:
|
|
446
|
+
|
|
447
|
+
```sql
|
|
448
|
+
WHERE (@searchText IS NULL OR @searchText = ''
|
|
449
|
+
OR json_extract(data, '$.name') LIKE '%' || @searchText || '%')
|
|
450
|
+
AND (@category IS NULL OR @category = ''
|
|
451
|
+
OR json_extract(data, '$.category') = @category)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**Pattern elements:**
|
|
455
|
+
|
|
456
|
+
| Element | Purpose |
|
|
457
|
+
| --- | --- |
|
|
458
|
+
| `@searchText IS NULL` | Handle null parameter |
|
|
459
|
+
| `@searchText = ''` | Handle empty string |
|
|
460
|
+
| `LIKE '%' \|\| @searchText \|\| '%'` | Partial match |
|
|
461
|
+
| `json_extract(data, '$.field')` | Access JSON fields |
|
|
462
|
+
|
|
463
|
+
### With External Package Data
|
|
464
|
+
|
|
465
|
+
Searchable dropdown with Acumatica or other package data:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// Create package data for Acumatica customers with search filter
|
|
469
|
+
const packageData = step.addPackageData({ package: 'my-data-package' })
|
|
470
|
+
packageData.addExecuteAction({ action: 'act-sync-customers' })
|
|
471
|
+
const customers = packageData.addDatasource({
|
|
472
|
+
datasourceId: 'customers-ds',
|
|
473
|
+
provider: 'local',
|
|
474
|
+
})
|
|
475
|
+
customers.table('Customers')
|
|
476
|
+
// Query with searchText filter for customer name and ID
|
|
477
|
+
customers.query(`
|
|
478
|
+
SELECT
|
|
479
|
+
json_extract(data, '$.CustomerID.value') AS value,
|
|
480
|
+
json_extract(data, '$.CustomerName.value') AS label,
|
|
481
|
+
json_extract(data, '$.Email.value') AS email
|
|
482
|
+
FROM ${customers.fullTableName}
|
|
483
|
+
WHERE @searchText IS NULL OR @searchText = ''
|
|
484
|
+
OR json_extract(data, '$.CustomerName.value') LIKE '%' || @searchText || '%'
|
|
485
|
+
OR json_extract(data, '$.CustomerID.value') LIKE '%' || @searchText || '%'
|
|
486
|
+
`)
|
|
487
|
+
// Create searchable dropdown, then bind searchText
|
|
488
|
+
const customerField = step.addDropdown({
|
|
489
|
+
name: 'customer',
|
|
490
|
+
label: 'Search Customer',
|
|
491
|
+
data: packageData,
|
|
492
|
+
isSearchable: true,
|
|
493
|
+
})
|
|
494
|
+
// Bind dropdown's searchText state to query parameter
|
|
495
|
+
customers.addQueryParameter(
|
|
496
|
+
'searchText',
|
|
497
|
+
new JsonataBuilder('$s', { s: customerField.state.searchText }),
|
|
498
|
+
)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Lookup Tables
|
|
502
|
+
|
|
503
|
+
Dynamic enum tables use the naming pattern `lookups-{name}`. Examples: `lookups-priorities`, `lookups-categories`, `lookups-departments`. The `lookups-` prefix distinguishes enum tables from app data tables. In SDK code, the table reference is `default/lookups-{name}`.
|
|
504
|
+
|
|
505
|
+
Tables are created automatically on first deploy — no manual setup required.
|
|
506
|
+
|
|
507
|
+
### Row Structure
|
|
508
|
+
|
|
509
|
+
Each row uses a human-readable slug as `rid` (not a GUID), with `label`, `sort`, and optional visual fields in content:
|
|
510
|
+
|
|
511
|
+
```json
|
|
512
|
+
{ "rid": "high", "content": { "label": "High", "sort": 1, "icon": "arrow-up" } }
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
- `rid`: slugified label — lowercase letters, numbers, and hyphens only (regex `^[a-z0-9-]+$`); use hyphens for spaces. `"High Priority"` → `high-priority`, `"In Progress"` → `in-progress`
|
|
516
|
+
- `label` (required): display text
|
|
517
|
+
- `sort` (required): sequential integer (1, 2, 3...) controlling display order
|
|
518
|
+
- `icon`, `image`, `avatar` (optional): visual metadata
|
|
519
|
+
|
|
520
|
+
### SQL Query for Lookups
|
|
521
|
+
|
|
522
|
+
```sql
|
|
523
|
+
SELECT id AS value, json_extract(data, '$.label') AS label FROM [default/lookups-{name}] ORDER BY json_extract(data, '$.sort')
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Seed Data Manifest
|
|
527
|
+
|
|
528
|
+
Initial lookup data is defined in `seed-data.json`, written alongside `app.ts`. After first deploy, the seed step populates tables automatically. Subsequent deploys skip seeding if the table already has rows.
|
|
529
|
+
|
|
530
|
+
```json
|
|
531
|
+
{
|
|
532
|
+
"tables": [
|
|
533
|
+
{
|
|
534
|
+
"tableId": "lookups-categories",
|
|
535
|
+
"rows": [
|
|
536
|
+
{ "rid": "electronics", "content": { "label": "Electronics", "sort": 1, "icon": "shopping-cart" } },
|
|
537
|
+
{ "rid": "clothing", "content": { "label": "Clothing", "sort": 2, "icon": "shirt" } },
|
|
538
|
+
{ "rid": "home-garden", "content": { "label": "Home & Garden", "sort": 3, "icon": "house" } }
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Each entry in `tables` has a `tableId` (e.g., `lookups-categories` — without the `default/` prefix) and an array of `rows` with `rid` and `content`.
|
|
546
|
+
|
|
547
|
+
If multiple dynamic enums exist, include all tables in a single `seed-data.json`. On resume/rebuild, overwrite the manifest entirely — it always reflects the current build intent.
|
|
548
|
+
|
|
549
|
+
## ArrayField Data Source
|
|
550
|
+
|
|
551
|
+
Populate dropdown from arrayField items collected in another step:
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// Step 1: Collect team members
|
|
555
|
+
const step1 = form.addStep({ instanceId: 'team' }, (step) => {
|
|
556
|
+
const members = step.addArrayField({
|
|
557
|
+
name: 'members',
|
|
558
|
+
label: 'Team Members',
|
|
559
|
+
})
|
|
560
|
+
members.with({
|
|
561
|
+
title: new JsonataBuilder('$n', { n: members.getCurrentItemRef('name') }),
|
|
562
|
+
})
|
|
563
|
+
members.addText({ name: 'name', label: 'Name' })
|
|
564
|
+
members.addEmail({ name: 'email', label: 'Email' })
|
|
565
|
+
})
|
|
566
|
+
step1.with({ icon: 'multiple-neutral-2', title: 'Team Members' })
|
|
567
|
+
// Step 2: Select from collected members
|
|
568
|
+
form.addStep({ instanceId: 'lead', icon: 'single-neutral' }, (step) => {
|
|
569
|
+
step.addDropdown({
|
|
570
|
+
name: 'teamLead',
|
|
571
|
+
label: 'Select Team Lead',
|
|
572
|
+
data: step1.data.getArrayFieldData('members'),
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Default mapping:**
|
|
578
|
+
|
|
579
|
+
| ArrayField Property | Dropdown Property |
|
|
580
|
+
| --- | --- |
|
|
581
|
+
| `id` (auto-generated UUID) | `value` |
|
|
582
|
+
| `item_title` (from arrayField.title) | `label` |
|
|
583
|
+
|
|
584
|
+
**Custom mapping:**
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
const step1 = form.addStep({ instanceId: 'team' }, (step) => {
|
|
588
|
+
const members = step.addArrayField({
|
|
589
|
+
name: 'members',
|
|
590
|
+
label: 'Team Members',
|
|
591
|
+
title: 'Member',
|
|
592
|
+
})
|
|
593
|
+
members.addText({ name: 'name', label: 'Name' })
|
|
594
|
+
members.addEmail({ name: 'email', label: 'Email' })
|
|
595
|
+
})
|
|
596
|
+
step1.with({ icon: 'multiple-neutral-2', title: 'Team Members' })
|
|
597
|
+
form.addStep({ instanceId: 'lead', icon: 'single-neutral' }, (step) => {
|
|
598
|
+
step.addDropdown({
|
|
599
|
+
name: 'teamLead',
|
|
600
|
+
label: 'Select Team Lead',
|
|
601
|
+
data: step1.data.getArrayFieldData('members', {
|
|
602
|
+
valueKey: 'email', // Use email as value
|
|
603
|
+
labelKey: 'name', // Use name as label
|
|
604
|
+
}),
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**Ref:** `./cross-step-data.md` for more on `getArrayFieldData()`
|
|
610
|
+
|
|
611
|
+
## Package Datasource
|
|
612
|
+
|
|
613
|
+
For data from external systems (ERP, CRM), use `addPackageData()` to sync and query package tables:
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
const packageData = step.addPackageData({ package: 'my-data-package' })
|
|
617
|
+
packageData.addExecuteAction({ action: 'act-sync-customers' })
|
|
618
|
+
const ds = packageData.addDatasource({
|
|
619
|
+
datasourceId: 'customers-ds',
|
|
620
|
+
provider: 'local',
|
|
621
|
+
})
|
|
622
|
+
ds.table('Customers')
|
|
623
|
+
ds.query(`
|
|
624
|
+
SELECT id AS value,
|
|
625
|
+
COALESCE(json_extract(data, '$.CustomerName.value'), json_extract(data, '$.CustomerID.value')) AS label
|
|
626
|
+
FROM ${ds.fullTableName}
|
|
627
|
+
`)
|
|
628
|
+
step.addDropdown({ name: 'customer', label: 'Customer', data: packageData })
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**Ref:** `./external-data-packages.md` for available tables, schemas, and sync actions
|
|
632
|
+
|
|
633
|
+
## Data Paths (labelDataPath / valueDataPath)
|
|
634
|
+
|
|
635
|
+
Specify JSON paths to label and value fields in datasource data.
|
|
636
|
+
|
|
637
|
+
| Property | Default | Description |
|
|
638
|
+
|----------|---------|-------------|
|
|
639
|
+
| `labelDataPath` | `'label'` | Path to display text |
|
|
640
|
+
| `valueDataPath` | `'value'` | Path to identifier |
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// Default behavior - uses 'label' and 'value' paths
|
|
644
|
+
step.addDropdown({
|
|
645
|
+
name: 'category',
|
|
646
|
+
label: 'Category',
|
|
647
|
+
data: categoriesData // labelDataPath: 'label', valueDataPath: 'value'
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
// Custom paths for external datasources with different schema
|
|
651
|
+
step.addDropdown({
|
|
652
|
+
name: 'customer',
|
|
653
|
+
label: 'Customer',
|
|
654
|
+
data: customers,
|
|
655
|
+
labelDataPath: 'data.CustomerName.value',
|
|
656
|
+
valueDataPath: 'id'
|
|
657
|
+
})
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**When to use:**
|
|
661
|
+
- Package data with custom schemas (check schema files in `external-data-packages`)
|
|
662
|
+
- Any datasource where fields aren't named `label` / `value`
|
|
663
|
+
|
|
664
|
+
## Multiple Selection
|
|
665
|
+
|
|
666
|
+
Enable multi-select with `isMultiple: true`:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
step.addDropdown({
|
|
670
|
+
name: 'skills',
|
|
671
|
+
label: 'Technical Skills',
|
|
672
|
+
isMultiple: true,
|
|
673
|
+
data: [
|
|
674
|
+
{ label: 'JavaScript', value: 'javascript', icon: 'java-script-logo' },
|
|
675
|
+
{ label: 'TypeScript', value: 'typescript', icon: 'type-script-logo' },
|
|
676
|
+
{ label: 'React', value: 'react', icon: 'react-native-logo' },
|
|
677
|
+
{ label: 'Node.js', value: 'nodejs', icon: 'server' },
|
|
678
|
+
],
|
|
679
|
+
})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Accessing Selected Dropdown Data
|
|
683
|
+
|
|
684
|
+
Dropdown fields expose two state properties:
|
|
685
|
+
|
|
686
|
+
| Property | Returns | Use Case |
|
|
687
|
+
| --- | --- | --- |
|
|
688
|
+
| `state.value` | `string \| number` | Selected value/id for conditions, business logic |
|
|
689
|
+
| `state.selected` | Full object | Access label, icon, or nested properties |
|
|
690
|
+
|
|
691
|
+
### state.value vs state.selected
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
const statusField = step.addDropdown({
|
|
695
|
+
name: 'status',
|
|
696
|
+
label: 'Status',
|
|
697
|
+
data: [
|
|
698
|
+
{ label: 'Active', value: 'active', icon: 'check-2' },
|
|
699
|
+
{ label: 'Inactive', value: 'inactive', icon: 'close' },
|
|
700
|
+
],
|
|
701
|
+
})
|
|
702
|
+
// state.value → "active" (just the value)
|
|
703
|
+
// Use for conditional logic based on selected value
|
|
704
|
+
step.addText({
|
|
705
|
+
name: 'statusNote',
|
|
706
|
+
label: 'Status Note',
|
|
707
|
+
isVisible: new JsonataBuilder('$status = "active"', {
|
|
708
|
+
status: statusField.state.value,
|
|
709
|
+
}),
|
|
710
|
+
})
|
|
711
|
+
// state.selected → { label: "Active", value: "active", icon: "check-2" }
|
|
712
|
+
// Use to access other properties like label or icon
|
|
713
|
+
step.addText({
|
|
714
|
+
name: 'statusLabel',
|
|
715
|
+
label: 'Selected Status Label',
|
|
716
|
+
value: new JsonataBuilder('$selected.label', {
|
|
717
|
+
selected: statusField.state.selected,
|
|
718
|
+
}),
|
|
719
|
+
isDisabled: true,
|
|
720
|
+
})
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Accessing External Package Data
|
|
724
|
+
|
|
725
|
+
With external datasources, use `state.selected` to access nested properties:
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
// Create package data for external package (e.g., Acumatica ERP customers)
|
|
729
|
+
const packageData = step.addPackageData({ package: 'my-data-package' })
|
|
730
|
+
packageData.addExecuteAction({ action: 'act-sync-customers' })
|
|
731
|
+
const customers = packageData.addDatasource({
|
|
732
|
+
datasourceId: 'customers-ds',
|
|
733
|
+
provider: 'local',
|
|
734
|
+
})
|
|
735
|
+
customers.table('Customers')
|
|
736
|
+
// Query selects value, label, and extra fields for state.selected access
|
|
737
|
+
customers.query(`
|
|
738
|
+
SELECT
|
|
739
|
+
json_extract(data, '$.CustomerID.value') AS value,
|
|
740
|
+
json_extract(data, '$.CustomerName.value') AS label,
|
|
741
|
+
json_extract(data, '$.Email.value') AS email
|
|
742
|
+
FROM ${customers.fullTableName}
|
|
743
|
+
`)
|
|
744
|
+
// Customer dropdown
|
|
745
|
+
const customerField = step.addDropdown({
|
|
746
|
+
name: 'customer',
|
|
747
|
+
label: 'Customer',
|
|
748
|
+
data: packageData,
|
|
749
|
+
})
|
|
750
|
+
// Derived read-only field showing customer email
|
|
751
|
+
// Uses state.selected to access the full selected object
|
|
752
|
+
step.addText({
|
|
753
|
+
name: 'customerEmail',
|
|
754
|
+
label: 'Customer Email',
|
|
755
|
+
value: new JsonataBuilder('$customer.email', {
|
|
756
|
+
customer: customerField.state.selected,
|
|
757
|
+
}),
|
|
758
|
+
isDisabled: true,
|
|
759
|
+
})
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
For derived fields in forms, prefer binding the target field directly to the dropdown's `state.selected.<Field>` instead of copying values through jig state with `action.set-jig-state`.
|
|
763
|
+
|
|
764
|
+
Example:
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
form.addControl.textField({
|
|
768
|
+
instanceId: 'UnitPrice',
|
|
769
|
+
label: 'Unit Price',
|
|
770
|
+
value: '=@ctx.components.InventoryID.state.selected.UnitPrice',
|
|
771
|
+
})
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
This keeps the form reactive to the currently selected dropdown row and avoids extra state propagation layers.
|
|
775
|
+
|
|
776
|
+
## Displaying Dropdown Labels
|
|
777
|
+
|
|
778
|
+
Dropdown fields store `value` (id) but often need to display `label` (human-readable text).
|
|
779
|
+
|
|
780
|
+
### getDropdownLabel() - Cross-Step Label Access
|
|
781
|
+
|
|
782
|
+
Access the persisted label for submission titles or cross-step display:
|
|
783
|
+
|
|
784
|
+
```typescript
|
|
785
|
+
const step1 = form.addStep({ instanceId: 'priority-step' }, (step) => {
|
|
786
|
+
step.addDropdown({
|
|
787
|
+
name: 'priority',
|
|
788
|
+
label: 'Priority',
|
|
789
|
+
data: [
|
|
790
|
+
{ label: 'High', value: 'high' },
|
|
791
|
+
{ label: 'Medium', value: 'medium' },
|
|
792
|
+
{ label: 'Low', value: 'low' },
|
|
793
|
+
],
|
|
794
|
+
})
|
|
795
|
+
})
|
|
796
|
+
step1.with({ title: 'Set Priority', icon: 'alert-triangle' })
|
|
797
|
+
// Use getDropdownLabel() to show "High" instead of "high" in submissions
|
|
798
|
+
form.with({
|
|
799
|
+
submissionItemTitle: new JsonataBuilder('$priority', {
|
|
800
|
+
priority: step1.data.getDropdownLabel('priority'),
|
|
801
|
+
}),
|
|
802
|
+
})
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
| Method | Returns | Use Case |
|
|
806
|
+
| --- | --- | --- |
|
|
807
|
+
| `step.data.getFieldData('dropdown')` | `string \| number` (value) | Business logic, conditions |
|
|
808
|
+
| `step.data.getDropdownLabel('dropdown')` | `string` or `string[]` | Display text, titles |
|
|
809
|
+
|
|
810
|
+
### Multi-Select Labels with $join()
|
|
811
|
+
|
|
812
|
+
For multi-select dropdowns, `getDropdownLabel()` returns an array of strings:
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
const step1 = form.addStep({ instanceId: 'tags-step' }, (step) => {
|
|
816
|
+
step.addDropdown({
|
|
817
|
+
name: 'tags',
|
|
818
|
+
label: 'Tags',
|
|
819
|
+
isMultiple: true,
|
|
820
|
+
data: [
|
|
821
|
+
{ label: 'Urgent', value: 'urgent' },
|
|
822
|
+
{ label: 'Bug', value: 'bug' },
|
|
823
|
+
{ label: 'Feature', value: 'feature' },
|
|
824
|
+
],
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
step1.with({ title: 'Select Tags', icon: 'tags' })
|
|
828
|
+
// Multi-select: getDropdownLabel returns array of strings ["Urgent", "Bug"]
|
|
829
|
+
// Use $join() to display as "Urgent, Bug"
|
|
830
|
+
form.with({
|
|
831
|
+
submissionItemTitle: new JsonataBuilder('$join($tags?$tags:[], ", ")', {
|
|
832
|
+
tags: step1.data.getDropdownLabel('tags'),
|
|
833
|
+
}),
|
|
834
|
+
})
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### getCurrentItemLabelRef() - ArrayField Context
|
|
838
|
+
|
|
839
|
+
Inside arrayField, use `getCurrentItemLabelRef()` for dropdown labels:
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
const items = step.addArrayField({
|
|
843
|
+
name: 'orderItems',
|
|
844
|
+
label: 'Order Items',
|
|
845
|
+
})
|
|
846
|
+
// Display dropdown LABEL (e.g., "Electronics") in item title
|
|
847
|
+
// instead of dropdown VALUE (e.g., "electronics")
|
|
848
|
+
items.with({
|
|
849
|
+
title: new JsonataBuilder('$product & " - " & $categoryLabel', {
|
|
850
|
+
product: items.getCurrentItemRef('productName'),
|
|
851
|
+
categoryLabel: items.getCurrentItemLabelRef('category'),
|
|
852
|
+
}),
|
|
853
|
+
})
|
|
854
|
+
items.addText({ name: 'productName', label: 'Product Name' })
|
|
855
|
+
items.addDropdown({
|
|
856
|
+
name: 'category',
|
|
857
|
+
label: 'Category',
|
|
858
|
+
data: [
|
|
859
|
+
{ label: 'Electronics', value: 'electronics' },
|
|
860
|
+
{ label: 'Clothing', value: 'clothing' },
|
|
861
|
+
{ label: 'Home & Garden', value: 'home' },
|
|
862
|
+
],
|
|
863
|
+
})
|
|
864
|
+
items.addNumber({ name: 'quantity', label: 'Quantity' })
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**Ref:** `./cross-step-data.md` for more on data access patterns
|
|
868
|
+
|
|
869
|
+
## Finding Examples
|
|
870
|
+
|
|
871
|
+
Use MCP to search for patterns:
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
// Search examples
|
|
875
|
+
mcp__jigx_expert_sdk__search_expert_sdk_examples({ search: "dropdown datasource" })
|
|
876
|
+
|
|
877
|
+
// Get specific example
|
|
878
|
+
mcp__jigx_expert_sdk__get_expert_sdk_example({ id: "..." })
|
|
879
|
+
```
|