@xeplr/actions 1.3.0 → 1.4.0
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 +4 -1
- package/lib/builtins/export/pages.js +16 -2
- package/lib/formats/writers/pdf.js +14 -6
- package/lib/registry.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ var moved = await actions.runAction(actions.builtins.dbMove, {
|
|
|
64
64
|
|---|---|
|
|
65
65
|
| `register(def)` / `register(name, def)` | Add an action. Throws without `name` or `execute`, or with a `requires` module that is not installed (`ActionMissingDependencyError`). Re-registering a name replaces it. |
|
|
66
66
|
| `get(name)` / `has(name)` | Look one up. |
|
|
67
|
-
| `list()` | `[{ name, description, inputSchema, outputSchema, requires }]` — serializable, for an actions endpoint. |
|
|
67
|
+
| `list()` | `[{ name, description, inputSchema, outputSchema, requires, event }]` — serializable, for an actions endpoint. `event` is `null` for an ordinary action. |
|
|
68
68
|
| `clear()` | Empty the registry. |
|
|
69
69
|
| `runAction({ name, input, system?, timeoutMs? })` | Run a registered action. Never throws. |
|
|
70
70
|
| `runAction(nameOrDef, input, { system, timeoutMs, metaStore }?)` | Same, by name or with an action module directly. |
|
|
@@ -91,6 +91,7 @@ var moved = await actions.runAction(actions.builtins.dbMove, {
|
|
|
91
91
|
| `outputSchema` | Optional documentation of the return value. |
|
|
92
92
|
| `requires` | Module names checked with `require.resolve` at `register()`. |
|
|
93
93
|
| `execute({ input, system, metaStore })` | Returns (or resolves to) the output. |
|
|
94
|
+
| `event` | Optional. Makes this a **screen function** rather than a step: the event it is written for — `beforeLoad`, `beforeSave`, `afterSave`, `beforeDelete`, `onError` (run by `@xeplr/factory` around a screen's records) or `decide` (run by `@xeplr/workflow-engine` to choose a flow's next step). A screen names one per event and pickers offer only the functions for that event. It is called with the event's context — `execute(ctx)`, not `{ input, system }` — and never as a workflow step. |
|
|
94
95
|
|
|
95
96
|
### `runAction` result
|
|
96
97
|
|
|
@@ -260,6 +261,8 @@ await runAction(builtins.exportTable, {
|
|
|
260
261
|
- `rect` (`fill`, `stroke`, `radius`)
|
|
261
262
|
- `line`
|
|
262
263
|
|
|
264
|
+
**`page: { width, height }`** (points, 72–14,400 each) makes every page exactly that size, edge to edge: no margins, no title and no footer or page numbers. It is for a PDF *of the screen*: a board captured as one image, posted as a single `image` block at `x: 0, y: 0, w: 1, h: 1` on a page the board's own size. Nothing is scaled, so what you see is what you get. Without `page`, the layout is A4 with margins, the title on page 1 and a footer on each page, as before.
|
|
265
|
+
|
|
263
266
|
What it is given is assumed to come from a browser, so it is bounded: 50 pages, 200 blocks a page, 8 MB per image, 500 table rows, and 5,000 characters of text. An image that will not decode draws a note in its box instead of failing the whole file.
|
|
264
267
|
|
|
265
268
|
Peers (optional, needed only by the format used): `fflate` for xlsx; `pdfkit` and `dejavu-fonts-ttf` for pdf. `writers` (`getWriter`, `INFO`, `SUPPORTED`) is exported for code that wants a writer without the action.
|
|
@@ -25,11 +25,22 @@ function fail(message) {
|
|
|
25
25
|
return err;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// A page of an exact size, in points: at least an inch, at most PDF's own
|
|
29
|
+
// 200-inch limit (14,400 pt) on a side.
|
|
30
|
+
function pageSize(page) {
|
|
31
|
+
if (page == null) return;
|
|
32
|
+
var ok = function(n) { return typeof n === 'number' && isFinite(n) && n >= 72 && n <= 14400; };
|
|
33
|
+
if (typeof page !== 'object' || !ok(page.width) || !ok(page.height)) {
|
|
34
|
+
throw fail('page must be { width, height } in points, each between 72 and 14400');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
function fraction(n) { return typeof n === 'number' && isFinite(n) && n >= -0.01 && n <= 50; }
|
|
29
39
|
|
|
30
40
|
/** Throws (400) naming the first block that is not one this action draws. */
|
|
31
41
|
function validate(spec) {
|
|
32
42
|
var pages = spec.pages;
|
|
43
|
+
pageSize(spec.page);
|
|
33
44
|
if (!Array.isArray(pages) || !pages.length) throw fail('pages must list at least one page');
|
|
34
45
|
if (pages.length > LIMITS.pages) throw fail('At most ' + LIMITS.pages + ' pages');
|
|
35
46
|
pages.forEach(function(page, p) {
|
|
@@ -66,7 +77,9 @@ module.exports = {
|
|
|
66
77
|
{ name: 'title', type: 'string', order: 2 },
|
|
67
78
|
{ name: 'footer', type: 'string', order: 3 },
|
|
68
79
|
{ name: 'landscape', type: 'boolean', order: 4, description: 'Default true.' },
|
|
69
|
-
{ name: 'outputPath', type: 'string', order: 5 }
|
|
80
|
+
{ name: 'outputPath', type: 'string', order: 5 },
|
|
81
|
+
{ name: 'page', type: 'object', order: 6,
|
|
82
|
+
description: '{ width, height } in points: every page exactly that size, edge to edge, no title or footer. For a PDF of what the screen shows.' }
|
|
70
83
|
],
|
|
71
84
|
outputSchema: [
|
|
72
85
|
{ name: 'filePath', type: 'string' }, { name: 'fileName', type: 'string' },
|
|
@@ -82,7 +95,8 @@ module.exports = {
|
|
|
82
95
|
fs.mkdirSync(dir, { recursive: true });
|
|
83
96
|
var filePath = input.outputPath || path.join(fs.mkdtempSync(path.join(dir, 'export-')), name);
|
|
84
97
|
var done = await pdf.renderPages(filePath, {
|
|
85
|
-
pages: input.pages, title: input.title, footer: input.footer, landscape: input.landscape !== false
|
|
98
|
+
pages: input.pages, title: input.title, footer: input.footer, landscape: input.landscape !== false,
|
|
99
|
+
page: input.page || null
|
|
86
100
|
});
|
|
87
101
|
return {
|
|
88
102
|
filePath: filePath, fileName: path.basename(filePath), contentType: 'application/pdf',
|
|
@@ -28,8 +28,8 @@ function newDocument(filePath, opts) {
|
|
|
28
28
|
var PDFDocument = require('pdfkit');
|
|
29
29
|
var doc = new PDFDocument({
|
|
30
30
|
size: opts.size || 'A4',
|
|
31
|
-
layout: opts.landscape ? 'landscape' : 'portrait',
|
|
32
|
-
margins: { top: 36, bottom: 36, left: 32, right: 32 },
|
|
31
|
+
layout: opts.size ? 'portrait' : (opts.landscape ? 'landscape' : 'portrait'),
|
|
32
|
+
margins: opts.margin === 0 ? { top: 0, bottom: 0, left: 0, right: 0 } : { top: 36, bottom: 36, left: 32, right: 32 },
|
|
33
33
|
bufferPages: true,
|
|
34
34
|
info: { Title: opts.title || 'Export', Producer: '@xeplr/actions' }
|
|
35
35
|
});
|
|
@@ -222,7 +222,13 @@ module.exports = {
|
|
|
222
222
|
* @param {object} spec - { pages: [{ blocks }], title?, footer?, landscape?: true, font?, boldFont? }
|
|
223
223
|
*/
|
|
224
224
|
renderPages: async function(filePath, spec) {
|
|
225
|
-
|
|
225
|
+
// spec.page — { width, height } in points: a page exactly that size, edge
|
|
226
|
+
// to edge, with no title and no footer. What a "PDF of the screen" needs:
|
|
227
|
+
// the content IS the page, so nothing is scaled, margined or stamped.
|
|
228
|
+
var exact = spec.page && spec.page.width && spec.page.height ? spec.page : null;
|
|
229
|
+
var made = newDocument(filePath, exact
|
|
230
|
+
? { title: spec.title, size: [exact.width, exact.height], margin: 0, font: spec.font, boldFont: spec.boldFont }
|
|
231
|
+
: { title: spec.title, landscape: spec.landscape !== false, font: spec.font, boldFont: spec.boldFont });
|
|
226
232
|
var pdf = made.doc;
|
|
227
233
|
var pages = spec.pages && spec.pages.length ? spec.pages : [{ blocks: [] }];
|
|
228
234
|
try {
|
|
@@ -230,8 +236,10 @@ module.exports = {
|
|
|
230
236
|
pages.forEach(function(page, p) {
|
|
231
237
|
if (p > 0) pdf.addPage();
|
|
232
238
|
var m = pdf.page.margins;
|
|
233
|
-
var box =
|
|
234
|
-
|
|
239
|
+
var box = exact
|
|
240
|
+
? { x: 0, y: 0, w: pdf.page.width, h: pdf.page.height }
|
|
241
|
+
: { x: m.left, y: m.top, w: pdf.page.width - m.left - m.right, h: pdf.page.height - m.top - m.bottom - 14 };
|
|
242
|
+
if (p === 0 && spec.title && !exact) {
|
|
235
243
|
pdf.font('bold').fontSize(13).fillColor(COLORS.text).text(spec.title, box.x, box.y, { width: box.w, lineBreak: false });
|
|
236
244
|
var used = pdf.y - box.y + 6;
|
|
237
245
|
box = { x: box.x, y: box.y + used, w: box.w, h: box.h - used };
|
|
@@ -281,7 +289,7 @@ module.exports = {
|
|
|
281
289
|
pdf.restore();
|
|
282
290
|
});
|
|
283
291
|
});
|
|
284
|
-
stampPages(pdf, spec.footer);
|
|
292
|
+
if (!exact) stampPages(pdf, spec.footer);
|
|
285
293
|
} catch (err) {
|
|
286
294
|
made.abandon();
|
|
287
295
|
throw err;
|
package/lib/registry.js
CHANGED
|
@@ -9,8 +9,17 @@
|
|
|
9
9
|
// outputSchema : array? — optional; documents what the action returns
|
|
10
10
|
// requires : string[]? — peer-dep module names checked at register time
|
|
11
11
|
// execute : function — async ({ input, system }) => output
|
|
12
|
+
// event : string? — a SCREEN FUNCTION, not a step: the event it
|
|
13
|
+
// is written for ('beforeSave', 'decide', …).
|
|
14
|
+
// Called with that event's own context instead
|
|
15
|
+
// of { input, system } — see the README.
|
|
12
16
|
// }
|
|
13
17
|
//
|
|
18
|
+
// One registry for both, because one app registers both and the two readers
|
|
19
|
+
// (a screen's events, a workflow's arrows and steps) live in one process. What
|
|
20
|
+
// an `event` means is the reader's business: this package only keeps it, and
|
|
21
|
+
// lists it, so a picker can offer each event only the functions written for it.
|
|
22
|
+
//
|
|
14
23
|
// Two call forms:
|
|
15
24
|
// register(name, def) → traditional
|
|
16
25
|
// register(def) → module-export style: `def.name` is used
|
|
@@ -33,6 +42,9 @@ function register(nameOrDef, def) {
|
|
|
33
42
|
if (typeof action.execute !== 'function') {
|
|
34
43
|
throw new Error('register: action "' + action.name + '" must have an `execute` function');
|
|
35
44
|
}
|
|
45
|
+
if (action.event !== undefined && (typeof action.event !== 'string' || !action.event.trim())) {
|
|
46
|
+
throw new Error('register: action "' + action.name + '" has an `event` that is not a name — e.g. "beforeSave"');
|
|
47
|
+
}
|
|
36
48
|
|
|
37
49
|
// Sanity-check peer deps.
|
|
38
50
|
if (Array.isArray(action.requires)) {
|
|
@@ -48,6 +60,7 @@ function register(nameOrDef, def) {
|
|
|
48
60
|
inputSchema: Array.isArray(action.inputSchema) ? action.inputSchema : [],
|
|
49
61
|
outputSchema: action.outputSchema || null,
|
|
50
62
|
requires: Array.isArray(action.requires) ? action.requires.slice() : [],
|
|
63
|
+
event: action.event || null,
|
|
51
64
|
execute: action.execute
|
|
52
65
|
});
|
|
53
66
|
}
|
|
@@ -64,7 +77,8 @@ function list() {
|
|
|
64
77
|
description: def.description,
|
|
65
78
|
inputSchema: def.inputSchema,
|
|
66
79
|
outputSchema: def.outputSchema,
|
|
67
|
-
requires: def.requires
|
|
80
|
+
requires: def.requires,
|
|
81
|
+
event: def.event
|
|
68
82
|
});
|
|
69
83
|
});
|
|
70
84
|
return out;
|
package/package.json
CHANGED