@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,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.
|