@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.
- package/README.md +2 -0
- 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 +5 -4
- package/CHANGELOG.md +0 -95
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Pattern: PDF Generation and Preview
|
|
2
|
+
|
|
3
|
+
Generate a PDF from form data, persist the file URI on the source record, preview in a native document viewer, share via the OS share sheet.
|
|
4
|
+
|
|
5
|
+
## The trio
|
|
6
|
+
|
|
7
|
+
1. **`app.script('html.js', ...)`** — JS helper that builds the HTML string (see [pattern-javascript-functions.md](pattern-javascript-functions.md))
|
|
8
|
+
2. **`action.generate-pdf`** — renders the HTML to a PDF file, outputs a local URI
|
|
9
|
+
3. **`jig.document` screen** — native PDF viewer that binds to the file URI
|
|
10
|
+
|
|
11
|
+
This pattern is **generic Jigx** — nothing Acumatica-specific.
|
|
12
|
+
|
|
13
|
+
## Global action — generate, save, push to state
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
export function actGenerateQuotePdf(app: ApplicationBuilder): void {
|
|
17
|
+
const action = app.addAction({ actionId: 'act-generate-quote-pdf' })
|
|
18
|
+
action.addParameter('opportunityID', { type: 'string', required: true })
|
|
19
|
+
action.addParameter('quoteName', { type: 'string', required: false })
|
|
20
|
+
|
|
21
|
+
const workflow = action.action.list({ concurrency: 'sequential' })
|
|
22
|
+
|
|
23
|
+
// 1. Generate the PDF
|
|
24
|
+
workflow.actions.generatePdf({
|
|
25
|
+
instanceId: 'generate-pdf',
|
|
26
|
+
fileName: '=@ctx.action.parameters.quoteName & "-" & @ctx.action.parameters.opportunityID & ".pdf"',
|
|
27
|
+
html: '=$html.salesFormGenerateHTML(@ctx.datasources.salesInfoAnswers, ...)',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// 2. Persist URI on the quote record (merge over existing fields).
|
|
31
|
+
//
|
|
32
|
+
// Two critical details:
|
|
33
|
+
// a. `when` guard — only save if the row actually exists. Otherwise a missing
|
|
34
|
+
// datasource or empty filter would cause $merge to produce an object with
|
|
35
|
+
// only {pdfFilePath, pdfGeneratedAt} and no id, triggering a *duplicate row*
|
|
36
|
+
// on every PDF generation.
|
|
37
|
+
// b. Explicitly override `id` with the row's top-level `.id` column (not
|
|
38
|
+
// `.data.id`). Jigx may strip `id` from the JSON blob on save and keep it
|
|
39
|
+
// only as the row's primary key column. Reading `.data.id` can return
|
|
40
|
+
// undefined, leaving the merged object with no id → new UUID → duplicate row.
|
|
41
|
+
workflow.actions
|
|
42
|
+
.executeEntity({
|
|
43
|
+
instanceId: 'save-pdf-path',
|
|
44
|
+
when: '=$exists(@ctx.datasources.salesInfoAnswers.id)',
|
|
45
|
+
})
|
|
46
|
+
.dynamicData('default/salesInfoAnswers', 'save')
|
|
47
|
+
.data(
|
|
48
|
+
'=$merge([@ctx.datasources.salesInfoAnswers.data, {"id": @ctx.datasources.salesInfoAnswers.id, "pdfFilePath": @ctx.actions.generate-pdf.outputs.uri, "pdfGeneratedAt": $now()}])',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// 3. Push URI to screen state for immediate viewer refresh
|
|
52
|
+
workflow.actions.setScreenState({
|
|
53
|
+
instanceId: 'set-pdf-path',
|
|
54
|
+
keys: { pdfFilePath: '=@ctx.actions.generate-pdf.outputs.uri' },
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Why the three steps matter
|
|
60
|
+
|
|
61
|
+
- **Step 1** produces the URI but nothing else is aware of it yet.
|
|
62
|
+
- **Step 2** writes it to the database. This triggers a datasource cache invalidation, so lists showing quotes will eventually refresh. But the current screen's datasources may not have re-queried yet by the time step 3 runs.
|
|
63
|
+
- **Step 3** puts the URI directly into screen state, which the viewer binds to first (before falling back to the datasource value). The viewer updates on the next render tick, no race.
|
|
64
|
+
|
|
65
|
+
### `$merge` over field-by-field enumeration
|
|
66
|
+
|
|
67
|
+
For upsert-style saves where most fields should be preserved and only a few updated, `$merge([existing, patch])` is much cleaner than listing every field:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Verbose (don't do this)
|
|
71
|
+
.data({
|
|
72
|
+
id: '=@ctx.datasources.X.data.id',
|
|
73
|
+
field1: '=@ctx.datasources.X.data.field1',
|
|
74
|
+
field2: '=@ctx.datasources.X.data.field2',
|
|
75
|
+
// ... 20 more fields
|
|
76
|
+
pdfFilePath: '=@ctx.actions.generate-pdf.outputs.uri',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Clean
|
|
80
|
+
.data('=$merge([@ctx.datasources.X.data, {"id": @ctx.datasources.X.id, "pdfFilePath": @ctx.actions.generate-pdf.outputs.uri}])')
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Always include `.id` explicitly in the merge patch.** Jigx's `save` method may strip
|
|
84
|
+
`id` from the JSON blob on write, keeping it only as the row primary key column.
|
|
85
|
+
Reading back via `.data.id` can return undefined, which breaks the upsert key and
|
|
86
|
+
creates a duplicate row. Override explicitly with the row's top-level `.id`.
|
|
87
|
+
|
|
88
|
+
## Passing datasource arrays to JavaScript functions
|
|
89
|
+
|
|
90
|
+
Single-row array datasources (non-`isDocument`) have a trap. JSONata's "singleton
|
|
91
|
+
sequence" behavior unwraps a 1-element sequence to a scalar when passed to a function:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
@ctx.datasources.doors = [{id, data: {...}}] // 1-row array
|
|
95
|
+
@ctx.datasources.doors.data = {...} ← scalar, not [{...}]!
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The JS function expecting an array will see a single object and `Array.isArray()`
|
|
99
|
+
returns false. Fix by forcing array shape with `[...]` wrapping:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
[@ctx.datasources.doors.data] = [{...}] // always an array
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For datasources where you need the row's top-level `id` preserved inside each item
|
|
106
|
+
(e.g., to match foreign keys like `answer.itemID === item.id`), use `$map` with
|
|
107
|
+
`$merge` to lift the id into the data object:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
$map(@ctx.datasources.salesChecklist, function($r) { $merge([{"id": $r.id}, $r.data]) })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each resulting item is `{id, title, description, ...}` — id promoted from the row
|
|
114
|
+
column into the data shape the JS helper expects.
|
|
115
|
+
|
|
116
|
+
`isDocument: true` datasources don't have this trap — they always return a single
|
|
117
|
+
object, so `@ctx.datasources.X.data` is reliably the data object.
|
|
118
|
+
|
|
119
|
+
## Redesigning an existing PDF safely
|
|
120
|
+
|
|
121
|
+
When replacing a production PDF layout, do not overwrite the existing HTML helper
|
|
122
|
+
until the new layout has been reviewed in the app. Add a second script file and
|
|
123
|
+
register it with its own namespace:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
app.script('quotehtml.js', readScriptFile('quotehtml.js')) // existing fallback
|
|
127
|
+
app.script('quotepresentation.js', readScriptFile('quotepresentation.js')) // new layout
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then change only the `action.generate-pdf.html` expression to call the new
|
|
131
|
+
namespace, keeping the argument order and datasource wrapping identical:
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
=$quotepresentation.salesPresentationGenerateHTML(
|
|
135
|
+
@ctx.datasources.salesInfoAnswers.data,
|
|
136
|
+
[@ctx.datasources.doorDetails.data],
|
|
137
|
+
@ctx.datasources.salesSelectionAnswers.data,
|
|
138
|
+
$map(@ctx.datasources.salesChecklist, function($r) { $merge([{"id": $r.id}, $r.data]) }),
|
|
139
|
+
[@ctx.datasources.salesChecklistAnswers.data],
|
|
140
|
+
[@ctx.datasources.openerDetails.data],
|
|
141
|
+
@ctx.datasources.salesOpenersAnswers.data,
|
|
142
|
+
[@ctx.datasources.salesFormSignatures.data]
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This keeps rollback cheap and avoids reintroducing array/singleton shape bugs
|
|
147
|
+
while changing only the presentation layer.
|
|
148
|
+
|
|
149
|
+
### Use line records as the pricing source of truth
|
|
150
|
+
|
|
151
|
+
For quote PDFs with doors, openers, products, services, or other repeatable line
|
|
152
|
+
items, keep `price`, `tax`, and `totalWithTax` on each line record. The quote
|
|
153
|
+
screen and PDF should aggregate those child records for running totals instead of
|
|
154
|
+
duplicating an editable quote-level total on the disclosure or signature screen.
|
|
155
|
+
|
|
156
|
+
This prevents stale totals when a line is copied, deleted, repriced, or edited
|
|
157
|
+
after quote acceptance is captured. The final PDF page can still show a quote
|
|
158
|
+
summary, but it should be derived from the saved line rows:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
door total = SUM(COALESCE(totalWithTax, price + tax, 0))
|
|
162
|
+
opener total = SUM(COALESCE(totalWithTax, price + tax, 0))
|
|
163
|
+
quote total = door total + opener total
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
If a line-level code is internal only, do not render it in customer-facing PDF
|
|
167
|
+
HTML. Save it locally and pass it to integration/sync actions as needed.
|
|
168
|
+
|
|
169
|
+
### Paginate repeatable quote lines deliberately
|
|
170
|
+
|
|
171
|
+
For presentation-style PDFs, do not assume all repeatable line items fit on one
|
|
172
|
+
slide/page. Chunk child records into a conservative fixed count and render more
|
|
173
|
+
slides as needed:
|
|
174
|
+
|
|
175
|
+
- Door configuration cards: 2 per slide.
|
|
176
|
+
- Opener configuration cards: 2 per slide.
|
|
177
|
+
- Included services / scope: separate slide from repeatable products.
|
|
178
|
+
|
|
179
|
+
This keeps the layout stable when a quote has 3, 5, or more line items and avoids
|
|
180
|
+
one overflowing mixed slide that combines repeatable products with scope text.
|
|
181
|
+
|
|
182
|
+
## Signature images in generated PDFs
|
|
183
|
+
|
|
184
|
+
Do not rely on persisted local signature paths for PDF HTML unless the PDF is being
|
|
185
|
+
generated on the same device that captured the signature. Paths like
|
|
186
|
+
`file:///data/...` or `/var/mobile/...` are sandbox-local and can point to nothing
|
|
187
|
+
after the quote syncs to a different device. Missing images can also make the native
|
|
188
|
+
PDF renderer wait until timeout.
|
|
189
|
+
|
|
190
|
+
For signatures that must appear in generated PDFs:
|
|
191
|
+
|
|
192
|
+
- Keep `signatureImage` as the local URI for `signatureField.initialValue`.
|
|
193
|
+
- Save a separate `signatureImagePortable` field with `conversions:
|
|
194
|
+
[{ property: 'signatureImagePortable', from: 'local-uri', to: 'data-uri' }]`.
|
|
195
|
+
- Re-run the save/conversion when `signatureImagePortable` is missing, even if
|
|
196
|
+
`signatureImage` did not change. Older rows or rows captured on another device
|
|
197
|
+
can have only the non-portable local URI.
|
|
198
|
+
- In the PDF HTML helper, use `signatureImagePortable` first, then only fall back to
|
|
199
|
+
current-device local paths if needed.
|
|
200
|
+
- If no portable image is available, render a text fallback such as "Signature on
|
|
201
|
+
file" instead of emitting a stale local `<img>` path or leaving an unexplained
|
|
202
|
+
blank area.
|
|
203
|
+
|
|
204
|
+
## Product and logo images in generated PDFs
|
|
205
|
+
|
|
206
|
+
Do not embed large static product images as base64/data-URI constants in a solution.
|
|
207
|
+
It inflates the solution payload, slows script parsing, makes YAML diffs unreadable,
|
|
208
|
+
and can force heavy memory usage during PDF generation. Prefer normal hosted image
|
|
209
|
+
URLs for static manufacturer/product examples when the app is online and the image
|
|
210
|
+
host is reliable.
|
|
211
|
+
|
|
212
|
+
Use portable `data:` images only for small or user-generated values that cannot be
|
|
213
|
+
fetched by URL, such as `signatureImagePortable`. If a remote product image fails
|
|
214
|
+
to load, the PDF should still be recoverable by regenerating after connectivity is
|
|
215
|
+
restored. Do not use stale device-local `file://` paths for images captured on a
|
|
216
|
+
different device.
|
|
217
|
+
|
|
218
|
+
## Saving generated HTML for renderer debugging
|
|
219
|
+
|
|
220
|
+
When native PDF output does not match the browser-rendered HTML, save the exact
|
|
221
|
+
HTML string to a dedicated dynamic debug table immediately before
|
|
222
|
+
`action.generate-pdf`. This separates template bugs from native renderer bugs.
|
|
223
|
+
|
|
224
|
+
Use a first-class table name like `default/htmldebug`, not a generic scratch table,
|
|
225
|
+
so it is easy to inspect in the device database or management tools:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
app.addDatabase.default.table('htmldebug')
|
|
229
|
+
|
|
230
|
+
const htmlExpr = '$quote.salesPresentationGenerateHTML(...)'
|
|
231
|
+
|
|
232
|
+
workflow.actions
|
|
233
|
+
.executeEntity({ instanceId: 'debug-html' })
|
|
234
|
+
.dynamicData('default/htmldebug', 'save')
|
|
235
|
+
.data(
|
|
236
|
+
`={"id": @ctx.action.parameters.opportunityID, "opportunityID": @ctx.action.parameters.opportunityID, "html": ${htmlExpr}, "generatedAt": $now(), "templateVersion": "presentation-v1"}`,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
workflow.actions.generatePdf({
|
|
240
|
+
instanceId: 'generate-pdf',
|
|
241
|
+
fileName: '=@ctx.action.parameters.opportunityID & "-" & $millis() & ".pdf"',
|
|
242
|
+
html: `=${htmlExpr}`,
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Keep the debug row keyed by the parent record id (`opportunityID`, service order
|
|
247
|
+
id, etc.) so regeneration replaces the previous debug HTML for that record.
|
|
248
|
+
|
|
249
|
+
## Document screen — native PDF viewer
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
const screen = app.addScreen.document({
|
|
253
|
+
screenId: 'jig-quote-pdf-preview',
|
|
254
|
+
title: 'Quote PDF',
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
screen.addInput({ name: 'opportunityID', type: 'string' })
|
|
258
|
+
screen.with({ state: { pdfFilePath: { initialValue: null } } })
|
|
259
|
+
|
|
260
|
+
const hasPdfStateExpr =
|
|
261
|
+
'@ctx.jig.state.pdfFilePath != null and @ctx.jig.state.pdfFilePath != ""'
|
|
262
|
+
|
|
263
|
+
// All datasources the PDF generator needs — scoped to this quote
|
|
264
|
+
screen.addDatasource.sqlite({ datasourceId: 'salesInfoAnswers', provider: 'dynamic' })
|
|
265
|
+
.entity('default/salesInfoAnswers')
|
|
266
|
+
.query(`SELECT id, data FROM [default/salesInfoAnswers] WHERE json_extract(data, '$.opportunityID') = @opportunityID`)
|
|
267
|
+
.queryParameter('opportunityID', '=@ctx.jig.inputs.opportunityID')
|
|
268
|
+
.jsonProperties('data')
|
|
269
|
+
.isDocument(true)
|
|
270
|
+
// ... more datasources
|
|
271
|
+
|
|
272
|
+
// Generate on first visit in this screen/device session if no local state URI exists yet.
|
|
273
|
+
// Persisted file:// URIs are device-local and must not be trusted across phones,
|
|
274
|
+
// app reinstalls, or sandbox changes.
|
|
275
|
+
screen.onFocus.executeAction({
|
|
276
|
+
action: 'act-generate-quote-pdf',
|
|
277
|
+
when: `=(${hasPdfStateExpr}) = false`,
|
|
278
|
+
parameters: {
|
|
279
|
+
opportunityID: '=@ctx.jig.inputs.opportunityID',
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Bind the PDF source only to the current screen/device URI. Do not fall back to
|
|
284
|
+
// persisted pdfFilePath here; it may point to another device's sandbox.
|
|
285
|
+
screen.source.pdf({
|
|
286
|
+
uri: '=@ctx.jig.state.pdfFilePath',
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// Header actions — regenerate and share
|
|
290
|
+
const header = screen.header()
|
|
291
|
+
|
|
292
|
+
header.actions.executeAction({
|
|
293
|
+
title: 'Regenerate',
|
|
294
|
+
icon: 'synchronize-arrows-1',
|
|
295
|
+
action: 'act-generate-quote-pdf',
|
|
296
|
+
parameters: { opportunityID: '=@ctx.jig.inputs.opportunityID' },
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
header.actions.share({
|
|
300
|
+
title: 'Share',
|
|
301
|
+
icon: 'share',
|
|
302
|
+
fileUri: '=@ctx.jig.state.pdfFilePath',
|
|
303
|
+
when: '=$exists(@ctx.jig.state.pdfFilePath)',
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Placeholder PDFs for incomplete flows
|
|
308
|
+
|
|
309
|
+
`jig.document` has one source type for the screen. If the screen is a PDF viewer,
|
|
310
|
+
do not navigate away just because the real PDF cannot be generated yet. Generate a
|
|
311
|
+
small temporary placeholder PDF on focus, set its URI into screen state, and keep
|
|
312
|
+
the real persisted `pdfFilePath` untouched.
|
|
313
|
+
|
|
314
|
+
Use this when a tab should remain open but the quote/report is not ready:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
focusActions.infoModal({
|
|
318
|
+
instanceId: 'incomplete-alert',
|
|
319
|
+
when: '=@ctx.expressions.allSectionsComplete = false',
|
|
320
|
+
modal: { title: 'Quote Not Complete', buttonText: 'OK' },
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
focusActions.generatePdf({
|
|
324
|
+
instanceId: 'generate-placeholder-pdf',
|
|
325
|
+
when: '=@ctx.expressions.allSectionsComplete = false',
|
|
326
|
+
fileName: '="Quote-Not-Generated-" & @ctx.jig.inputs.opportunityID & ".pdf"',
|
|
327
|
+
html: '<html><body><h1>Not generated yet</h1></body></html>',
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
focusActions.setScreenState({
|
|
331
|
+
instanceId: 'set-placeholder-pdf',
|
|
332
|
+
when: '=@ctx.expressions.allSectionsComplete = false',
|
|
333
|
+
keys: { pdfFilePath: '=@ctx.actions.generate-placeholder-pdf.outputs.uri' },
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Hide `Share` and `Clear` while showing a placeholder. Those actions should apply
|
|
338
|
+
only to a real generated PDF, not the temporary screen-state PDF.
|
|
339
|
+
|
|
340
|
+
### Key behaviors to remember
|
|
341
|
+
|
|
342
|
+
- **`DocumentScreenBuilder extends ScreenBaseBuilderNew`** — so it has `addInput`, `addDatasource`, `with({state})`, `onFocus`, `header`, etc. Same building blocks as a default screen.
|
|
343
|
+
- **`source.pdf({uri})` accepts an expression** — reactive binding. When the expression changes (state, datasource), the viewer updates.
|
|
344
|
+
- **`onFocus` fires on navigation *to* the screen, not on `goBack`**. Safe to use for "generate if missing" without re-triggering every time the user leaves and comes back via back navigation.
|
|
345
|
+
- **`share` action requires a local `file://` URI** — base64 and data URIs don't work.
|
|
346
|
+
- **Generated PDF `file://` URIs are device-local.** A URI persisted on a dynamic-data row can point to another device's app sandbox, or to a previous install's sandbox. The preview screen should generate a fresh local PDF into screen state when `@ctx.jig.state.pdfFilePath` is missing, bind `source.pdf` to state only, and share state only. Persist `pdfFilePath` for audit/status/sync metadata, not as a guaranteed reusable viewer source.
|
|
347
|
+
- **Do not use `$exists` alone on initialized state.** If `pdfFilePath` is declared with `initialValue: null`, `$exists(@ctx.jig.state.pdfFilePath)` can be true even though there is no usable URI. Use an explicit non-null/non-empty check like `@ctx.jig.state.pdfFilePath != null and @ctx.jig.state.pdfFilePath != ""` for on-focus generation and share enablement.
|
|
348
|
+
- **Images inside generated HTML must not use stale device-local paths.** Device-local image/signature paths from another device can leave the PDF renderer waiting for images until `RENDER_TIMEOUT`. For signatures, store a portable `data:` representation or omit the image and show the signature/name/date fields instead.
|
|
349
|
+
- **External HTTP images can still fail if connectivity or the image host is bad.** Keep product imagery hosted at stable URLs and avoid very large images, but do not base64-embed large static brand/product photos into the solution.
|
|
350
|
+
- **Guard post-generation saves.** If `action.generate-pdf` fails, later sequential actions may still run. Save `pdfFilePath` and update screen state only when `$exists(@ctx.actions.generate-pdf.outputs.uri)` is true.
|
|
351
|
+
- **Give regenerate actions explicit feedback.** A `generate-pdf` failure can leave
|
|
352
|
+
`outputs.uri` empty while later guarded steps are skipped. For user-triggered
|
|
353
|
+
regeneration, add success and failure `infoModal` actions gated by
|
|
354
|
+
`$exists(@ctx.actions.<instanceId>.outputs.uri)` so the user knows whether a new
|
|
355
|
+
file was created.
|
|
356
|
+
- **Do not rely on print-only CSS for production appearance.** The current iOS
|
|
357
|
+
`action.generate-pdf` path renders HTML through the native Jigx PDF generator
|
|
358
|
+
using a WebView PDF export, not a full browser print flow. `@page`,
|
|
359
|
+
`page-break-after`, and `@media print` are useful hints, but normal screen CSS
|
|
360
|
+
must already look acceptable because iOS can export the scrollable WebView as a
|
|
361
|
+
single tall page. Avoid dark-only presentation slides, oversized decorative
|
|
362
|
+
absolute shapes, and backgrounds that only get corrected inside `@media print`.
|
|
363
|
+
- **Prefer flat, explicit PDF styling.** Native and desktop PDF viewers can render
|
|
364
|
+
transparency, gradients, and CSS shadows differently. For customer-facing PDFs,
|
|
365
|
+
prefer `background: #ffffff`, solid borders, and simple accent colors over
|
|
366
|
+
`rgba()`, `linear-gradient()`, and `box-shadow`. This keeps Android preview,
|
|
367
|
+
shared PDFs, macOS Preview, and browser print output closer to each other.
|
|
368
|
+
|
|
369
|
+
## Global action caller context
|
|
370
|
+
|
|
371
|
+
A key thing to understand: when `executeAction` calls a global action, expressions inside the global action's steps resolve in the **caller's screen context**. That means `@ctx.datasources.X` refers to the caller's screen-level datasources, not anything defined on the action itself (global actions don't have datasources).
|
|
372
|
+
|
|
373
|
+
So the PDF generation action above works because the PDF preview screen defines all the required datasources in its own scope. If you called the same action from a screen without those datasources loaded, the expressions would resolve to null.
|
|
374
|
+
|
|
375
|
+
## `file://` URIs flow end-to-end
|
|
376
|
+
|
|
377
|
+
For this pattern we deliberately store signature and media images as **local URIs** (not base64), then let the generator embed them directly as `<img src="file://...">`. The Jigx `generate-pdf` action renders these correctly. This avoids the cost of base64 encoding on every save and keeps the rows small. See "Signature and media storage" in [pattern-datasources.md](pattern-datasources.md) for the trade-off.
|
|
378
|
+
|
|
379
|
+
## Filename — namespace by script file name
|
|
380
|
+
|
|
381
|
+
Calling `app.script('html.js', ...)` registers the functions under the `$html` JSONata namespace (filename minus `.js`). Call as `$html.salesFormGenerateHTML(...)`. If you name the file `pdf.js`, call as `$pdf.create(...)`.
|
|
382
|
+
|
|
383
|
+
## Real-world reference
|
|
384
|
+
|
|
385
|
+
`workspace/acumatica-apps/doorpro-door-quote/` — the full trio:
|
|
386
|
+
|
|
387
|
+
- `src/scripts/html.js` — HTML helper
|
|
388
|
+
- `src/actions/act-generate-quote-pdf.ts` — global action
|
|
389
|
+
- `src/screens/jig-quote-pdf-preview.ts` — document screen
|
|
390
|
+
- `src/app.ts` — `readScriptFile` + `app.script('html.js', ...)`
|
|
391
|
+
- `src/screens/jig-quote-list.ts:93` — swipe-left that navigates to the PDF screen
|