@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,304 @@
1
+ # Pattern: Navigation
2
+
3
+ ## Screen lifecycle — `onFocus` vs `onLoad`
4
+
5
+ - **`onFocus`** fires when the user navigates *to* a screen (via `goto`, a tab switch,
6
+ or a list-item onPress). It does **not** fire when the user returns to the screen
7
+ via `goBack` from another screen. This is what you want for "run something each
8
+ time I arrive fresh", e.g., pushing input values into solution state, or triggering
9
+ an auto-generate action.
10
+ - **`onLoad`** fires once when the screen is first instantiated and never again.
11
+
12
+ Practical implication: if you use `screen.onFocus.setApplicationState({...})` to
13
+ push jig.inputs into solution state, the state is set on each navigation-in. But
14
+ if the user is already on the screen and navigates back to it, onFocus does not
15
+ re-fire — existing solution state from the previous visit is preserved.
16
+
17
+ For a "generate if missing" pattern (e.g., PDF auto-generation on screen focus),
18
+ always pair `onFocus` with a `when` guard so re-focusing a screen that already has
19
+ the result doesn't re-run the work:
20
+
21
+ ```typescript
22
+ screen.onFocus.executeAction({
23
+ action: 'act-generate-quote-pdf',
24
+ when: '=$exists(@ctx.datasources.salesInfoAnswers.data.pdfFilePath) = false',
25
+ parameters: { opportunityID: '=@ctx.jig.inputs.opportunityID' },
26
+ })
27
+ ```
28
+
29
+ ## Navigation with fresh instances
30
+
31
+ When navigating to a create form from a list, use `behaviour: 'new'` and a UUID instance:
32
+
33
+ ```typescript
34
+ screen.bottomPanel().add.goto({
35
+ title: 'New Customer',
36
+ linkTo: SCREEN.CUSTOMER_CREATE,
37
+ isPrimary: true,
38
+ behaviour: 'new',
39
+ instanceId: '=$uuid()',
40
+ })
41
+ ```
42
+
43
+ For standalone create screens, the generated UUID can be used as `@ctx.jig.instanceId` on the target screen and then saved as the new record `id`.
44
+
45
+ For child-create screens inside a parent aggregate, do not rely on the child screen `@ctx.jig.instanceId` as the row id. Generate the child row id at the `go-to` call and pass it in explicitly:
46
+
47
+ ```typescript
48
+ buttons.add.goto({
49
+ title: 'Add Detail',
50
+ linkTo: SCREEN.DETAIL_CREATE,
51
+ behaviour: 'new',
52
+ parameters: {
53
+ serviceOrderId: '=@ctx.jig.inputs.serviceOrderId',
54
+ detailId: '=$uuid()',
55
+ },
56
+ })
57
+ ```
58
+
59
+ Then save the child row with the passed `detailId`.
60
+
61
+ ## Bottom panel action order
62
+
63
+ The first action in `bottomPanel()` is the visible button. Additional actions go into the overflow menu (accessed via `...`).
64
+
65
+ ```typescript
66
+ const buttons = tabs.bottomPanel()
67
+ buttons.add.executeAction({ title: 'Save', isPrimary: true, ... }) // Visible
68
+ buttons.add.goBack({ title: 'Cancel' }) // Overflow
69
+ ```
70
+
71
+ ## Navigation flow: List → Detail → Child form
72
+
73
+ ```
74
+ jig-customer-list (jig.list)
75
+ ├── onPress → jig-customer-view (parameters: { customerId })
76
+ │ └── Add Contact → jig-contact-create (parameters: { customerId })
77
+ └── New Customer → jig-customer-create (tabs, behaviour: 'new', instanceId: $uuid())
78
+ ```
79
+
80
+ ### List item to detail
81
+
82
+ ```typescript
83
+ listItem.onPress.goto({
84
+ linkTo: SCREEN.CUSTOMER_VIEW,
85
+ parameters: { customerId: '=@ctx.current.item.id' },
86
+ })
87
+ ```
88
+
89
+ ### Detail to child form (forwarding parent ID)
90
+
91
+ ```typescript
92
+ buttons.add.goto({
93
+ title: 'Add Contact',
94
+ linkTo: SCREEN.CONTACT_CREATE,
95
+ parameters: { customerId: '=@ctx.jig.inputs.customerId' },
96
+ })
97
+ ```
98
+
99
+ ### Child form save with foreign key
100
+
101
+ For child entities (e.g., Contact belongs to Customer), use the parent's local ID as the relationship key. Before the parent syncs with the remote system, this is the jig `instanceId` (UUID). After sync, the REST function's find-replace operation updates this value to the remote system's business key.
102
+
103
+ ```typescript
104
+ // Contact save expression — foreign key links to parent
105
+ "BusinessAccount": {"value": @ctx.jig.inputs.customerId},
106
+ "CompanyName": {"value": @ctx.jig.inputs.customerName},
107
+ ```
108
+
109
+ **Acumatica-specific**: `BusinessAccount.value` is Acumatica's field linking a Contact to a Customer. The `{ "value": ... }` wrapper is Acumatica's field format. For other systems, use the appropriate foreign key field name and format.
110
+
111
+ The parent's REST function includes a find-replace on the child table to update the local UUID to the remote key after sync. See [pattern-rest-acumatica.md](pattern-rest-acumatica.md) for details.
112
+
113
+ ### Adding child records from a tab
114
+
115
+ When a tabs screen embeds a child list tab, pass the parent ID and name via tab inputs:
116
+
117
+ ```typescript
118
+ const contactsTab = tabs.addInstance.tab(SCREEN.CUSTOMER_CONTACTS, TAB_INSTANCE.CONTACTS)
119
+ contactsTab.input('customerId', '=@ctx.jig.instanceId')
120
+ contactsTab.input('customerName', '=@ctx.jigs.general-instance.components.CustomerName.state.value')
121
+ ```
122
+
123
+ The child list tab opens a modal form to create new records:
124
+
125
+ ```typescript
126
+ screen.bottomPanel().add.goto({
127
+ title: 'Add Contact',
128
+ linkTo: SCREEN.CONTACT_CREATE,
129
+ isPrimary: true,
130
+ isModal: true,
131
+ parameters: {
132
+ customerId: '=@ctx.jig.inputs.customerId',
133
+ customerName: '=@ctx.jig.inputs.customerName',
134
+ },
135
+ })
136
+ ```
137
+
138
+ If the child row needs its own stable local id before save, generate that id in the `go-to` parameters as well instead of trying to derive it from the child screen instance.
139
+
140
+ ## Sync status tags on list items
141
+
142
+ Show the sync state of each record using colored tags:
143
+
144
+ ```typescript
145
+ const listItem = screen.addControl
146
+ .listItem({ instanceId: 'customer-item' })
147
+ .title('=@ctx.current.item.data.CustomerName.value') // Acumatica field path
148
+ .subtitle('=@ctx.current.item.data.MainContact.Email.value')
149
+
150
+ listItem.addTag({
151
+ text: '=@ctx.current.item.data.Remote = "remote" ? "Synced" : @ctx.current.item.data.Remote = "dirty" ? "Edited" : "New"',
152
+ color: '=@ctx.current.item.data.Remote = "remote" ? "positive" : @ctx.current.item.data.Remote = "dirty" ? "warning" : "primary"',
153
+ })
154
+ ```
155
+
156
+ The `Remote` field and tag logic is generic — works with any system. The field paths in `title`/`subtitle` are **Acumatica-specific** (`.value` wrappers). Adapt to match your system's data structure.
157
+
158
+ | Remote value | Tag text | Color |
159
+ | --- | --- | --- |
160
+ | `"remote"` | Synced | `positive` (green) |
161
+ | `"dirty"` | Edited | `warning` (orange) |
162
+ | `"new"` | New | `primary` (blue) |
163
+
164
+ Apply to both Customer and Contact list items.
165
+
166
+ ## Detail screen with screen-scoped datasources
167
+
168
+ For detail/view screens, use screen-scoped datasources (declared on the screen, not at app level). These stay inline in the jig's YAML output. Follow the standard datasource pattern (`SELECT id, data` + `jsonProperties`).
169
+
170
+ ```typescript
171
+ const screen = app.addScreen.default({ screenId: 'jig-customer-view', title: 'Customer Detail' })
172
+ screen.addInput({ name: 'customerId', type: 'string', isRequired: true })
173
+
174
+ // Single document datasource
175
+ screen.addDatasource
176
+ .sqlite({ datasourceId: 'data-select-customer', provider: 'dynamic' })
177
+ .entity('default/customers')
178
+ .query('SELECT id, data FROM [default/customers] WHERE id = @customerId')
179
+ .queryParameter('customerId', '=@ctx.jig.inputs.customerId')
180
+ .jsonProperties('data')
181
+ .isDocument(true)
182
+
183
+ // Related records (json_extract only in WHERE)
184
+ screen.addDatasource
185
+ .sqlite({ datasourceId: 'data-select-contacts', provider: 'dynamic' })
186
+ .entity('default/contacts')
187
+ .query(`SELECT id, data FROM [default/contacts] WHERE json_extract(data, '$.customerId') = @customerId`)
188
+ .queryParameter('customerId', '=@ctx.jig.inputs.customerId')
189
+ .jsonProperties('data')
190
+ ```
191
+
192
+ Use `.isDocument(true)` for single-record lookups — returns an object instead of an array, so `=@ctx.datasources.data-select-customer.data.CustomerName.value` works directly (no `[0]` needed).
193
+
194
+ ## Swipe actions with confirmation + global action
195
+
196
+ List items support `swipeLeft()` and `swipeRight()`. Each returns a `SwipeableActionBuilder` whose `.onPress` is a full action builder — you can chain `confirm` (for destructive ops) then `executeAction` (for the actual work) via `onConfirmed`.
197
+
198
+ ```typescript
199
+ item
200
+ .swipeRight()
201
+ .label('Delete')
202
+ .icon('delete-1')
203
+ .color('negative')
204
+ .onPress.confirm({
205
+ modal: {
206
+ title: 'Delete Quote?',
207
+ description: '=@ctx.current.item.quoteName & " will be permanently deleted."',
208
+ confirm: 'Delete',
209
+ cancel: 'Cancel',
210
+ },
211
+ })
212
+ .onConfirmed.executeAction({
213
+ action: ACTION.DELETE_QUOTE,
214
+ parameters: {
215
+ opportunityID: '=@ctx.current.item.opportunityID',
216
+ },
217
+ })
218
+ ```
219
+
220
+ `@ctx.current.item.*` is available inside the swipe action (same scope as the list item itself), but **not** inside the global action it calls — the global action only sees the parameters you pass in. Pass anything the action needs (ids, names, etc.) explicitly.
221
+
222
+ ### Copying list rows locally
223
+
224
+ For local child rows, copy by saving a merged data object with a fresh row id and
225
+ fresh parent/line metadata. Keep the copied row tied to the same parent key unless
226
+ the copy is intentionally moving between parents.
227
+
228
+ ```typescript
229
+ item
230
+ .swipeLeft()
231
+ .label('Copy')
232
+ .icon('copy-1')
233
+ .color('positive')
234
+ .onPress.confirm({
235
+ modal: { title: 'Copy Item?', confirm: 'Copy', cancel: 'Cancel' },
236
+ })
237
+ .onConfirmed.executeEntity({ instanceId: 'copy-item' })
238
+ .dynamicData('default/childRows', 'save')
239
+ .data(
240
+ '=$merge([@ctx.current.item.data, {"id": $uuid(), "parentId": @ctx.jig.inputs.parentId, "lineNumber": @ctx.datasources.nextLineNumber.lineNumber}])',
241
+ )
242
+ ```
243
+
244
+ ### Add-next-item chooser
245
+
246
+ When a parent can contain multiple similar configured items (doors, openers, contacts,
247
+ etc.), the add action should branch:
248
+
249
+ - If no prior child rows exist, navigate directly to a clean create screen with a new
250
+ child id generated in the `go-to` parameters.
251
+ - If at least one child row exists, navigate to a small chooser screen that offers
252
+ "Copy previous" and "Start clean".
253
+ - "Copy previous" should save a merged local row with the new id/line metadata before
254
+ navigating to edit that copied row.
255
+ - "Start clean" should navigate with the same generated child id but no copied data.
256
+
257
+ This pattern keeps repeated configuration fast without reusing the previous row's id.
258
+
259
+ When adding a new child row and an existing row can act as a template, navigate to
260
+ a small chooser screen rather than overloading a confirm dialog. `action.confirm`
261
+ only has an `onConfirmed` branch, so it cannot implement both "copy previous" and
262
+ "start clean" from the cancel button. Use two explicit actions/list items instead.
263
+
264
+ ## Conditional tag on list item
265
+
266
+ `addTag({text, color, when})` supports a `when` expression — the tag is only rendered
267
+ when the expression evaluates truthy. This is the preferred pattern for conditional
268
+ badges on list items:
269
+
270
+ ```typescript
271
+ // Good — tag only shown when the condition is true
272
+ item.addTag({
273
+ text: 'PDF',
274
+ color: 'positive',
275
+ when: '=$exists(@ctx.current.item.pdfFilePath)',
276
+ })
277
+ ```
278
+
279
+ Note: at one point the SDK's `TagBadge` type definition was missing the `when` field,
280
+ which led to a workaround using the expression-based `.tags(expr)` method. That
281
+ workaround still works but is unnecessary — `addTag` with `when` is simpler and more
282
+ readable. If the TS type complains about `when`, update `packages/core-sdk/src/component/jc.list-item.ts`
283
+ to include `when?: BooleanOrExpression` on the `TagBadge` interface.
284
+
285
+ ### Row-specific tags driven by external datasources
286
+
287
+ When a static menu/list row shows status derived from separate datasources
288
+ (`formProgress`, checklist answers, child counts, etc.), prefer `.tags(expr)` and
289
+ return an array for the current row. This keeps the row identity and status logic
290
+ in one expression and avoids accidental repeated badges when several row types
291
+ share one compound condition.
292
+
293
+ ```typescript
294
+ item.tags(
295
+ '=(' +
296
+ '(@ctx.current.item.id = "details" and $count(@ctx.datasources.formProgress[itemSection="details" and $string(isCompleted)="1"]) > 0) or ' +
297
+ '(@ctx.current.item.id = "checklist" and $count(@ctx.datasources.requiredItems) > 0 and $count(@ctx.datasources.requiredItems) = $count(@ctx.datasources.answers))' +
298
+ ') ? [{"text":"Complete","color":"positive"}] : []',
299
+ )
300
+ ```
301
+
302
+ Use a stable row key such as `@ctx.current.item.id` rather than comparing display
303
+ labels. Coerce saved flag values with `$string(...)` or `$number(...)` when the
304
+ local table stores boolean-like values as strings.