@jigx/core-sdk 1.0.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.
Files changed (41) hide show
  1. package/README.md +2 -0
  2. package/dist/action/ja.generate-pdf.d.ts +17 -1
  3. package/dist/action/ja.generate-pdf.d.ts.map +1 -1
  4. package/dist/action/ja.generate-pdf.js +4 -1
  5. package/dist/action/ja.in-background.d.ts +3 -2
  6. package/dist/action/ja.in-background.d.ts.map +1 -1
  7. package/dist/action/ja.in-background.js +1 -1
  8. package/dist/assets/example-extraction-cache.json +3 -3
  9. package/dist/assets/extracted-core-sdk-examples.yaml +18 -0
  10. package/dist/assets/extracted-core-sdk-types.yaml +58 -0
  11. package/dist/assets/type-extraction-cache.json +3 -3
  12. package/docs/array-fields.md +371 -0
  13. package/docs/conditional-logic.md +178 -0
  14. package/docs/convention-naming.md +102 -0
  15. package/docs/date-field.md +92 -0
  16. package/docs/dropdown-fields.md +879 -0
  17. package/docs/field-state.md +131 -0
  18. package/docs/field-types-overview.md +132 -0
  19. package/docs/formatting.md +421 -0
  20. package/docs/icons.md +142 -0
  21. package/docs/index.md +23 -0
  22. package/docs/jsonata-expressions.md +200 -0
  23. package/docs/media-fields.md +107 -0
  24. package/docs/overview.md +467 -0
  25. package/docs/pattern-build-deploy.md +91 -0
  26. package/docs/pattern-datasources.md +459 -0
  27. package/docs/pattern-forms.md +528 -0
  28. package/docs/pattern-global-actions.md +92 -0
  29. package/docs/pattern-javascript-functions.md +452 -0
  30. package/docs/pattern-navigation.md +304 -0
  31. package/docs/pattern-pdf-generation.md +391 -0
  32. package/docs/pattern-rest-acumatica.md +660 -0
  33. package/docs/pattern-sync-progress.md +96 -0
  34. package/docs/pattern-sync.md +653 -0
  35. package/docs/pattern-tabs-form.md +293 -0
  36. package/docs/recipe-index.md +64 -0
  37. package/docs/runtime-variables.md +127 -0
  38. package/docs/sections.md +81 -0
  39. package/docs/validation-patterns.md +150 -0
  40. package/package.json +5 -4
  41. package/CHANGELOG.md +0 -95
@@ -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
+ ```