@asteby/metacore-runtime-react 28.7.0 → 29.0.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/CHANGELOG.md +43 -0
- package/dist/action-modal-dispatcher.js +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +13 -1
- package/dist/dynamic-relation.d.ts +4 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +80 -16
- package/dist/dynamic-relations.d.ts +22 -1
- package/dist/dynamic-relations.d.ts.map +1 -1
- package/dist/dynamic-relations.js +16 -3
- package/dist/entity-select.d.ts +43 -0
- package/dist/entity-select.d.ts.map +1 -0
- package/dist/entity-select.js +124 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/print-document-button.d.ts +21 -0
- package/dist/print-document-button.d.ts.map +1 -0
- package/dist/print-document-button.js +32 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-print-document.d.ts +25 -0
- package/dist/use-print-document.d.ts.map +1 -0
- package/dist/use-print-document.js +79 -0
- package/package.json +3 -3
- package/src/__tests__/relation-embed-gate.test.tsx +110 -0
- package/src/action-modal-dispatcher.tsx +3 -1
- package/src/dialogs/dynamic-record.tsx +6 -1
- package/src/dynamic-columns.tsx +14 -1
- package/src/dynamic-relation.tsx +121 -23
- package/src/dynamic-relations.tsx +34 -2
- package/src/entity-select.tsx +316 -0
- package/src/index.ts +5 -0
- package/src/print-document-button.tsx +75 -0
- package/src/types.ts +9 -0
- package/src/use-print-document.ts +112 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// PrintDocumentButton — a drop-in button that prints/downloads a server-rendered
|
|
2
|
+
// document via usePrintDocument, so an addon doesn't re-wire the hook + loading
|
|
3
|
+
// state every time. Headless-friendly: it renders a plain <button> you style
|
|
4
|
+
// with `className`, disables itself while the PDF is fetching, and reports
|
|
5
|
+
// failures through `onError` (no toast dependency baked in).
|
|
6
|
+
//
|
|
7
|
+
// Example:
|
|
8
|
+
// <PrintDocumentButton model="SalesOrder" id={sale.id} documentKey="sale_ticket"
|
|
9
|
+
// className="btn">Imprimir ticket</PrintDocumentButton>
|
|
10
|
+
import React, { useCallback, useState } from 'react'
|
|
11
|
+
import { usePrintDocument, type PrintDocumentArgs } from './use-print-document'
|
|
12
|
+
|
|
13
|
+
export interface PrintDocumentButtonProps
|
|
14
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onError'> {
|
|
15
|
+
/** Model KEY the document is declared against (e.g. "SalesOrder"). */
|
|
16
|
+
model: string
|
|
17
|
+
/** Record id. */
|
|
18
|
+
id: string
|
|
19
|
+
/** Document key from contributions.documents[].key (e.g. "sale_ticket"). */
|
|
20
|
+
documentKey: string
|
|
21
|
+
/** print (default) | download | open — see usePrintDocument. */
|
|
22
|
+
mode?: PrintDocumentArgs['mode']
|
|
23
|
+
/** Download filename (mode="download"). */
|
|
24
|
+
filename?: string
|
|
25
|
+
/** Called if the fetch/print fails (surface it to a toast in the host). */
|
|
26
|
+
onError?: (err: unknown) => void
|
|
27
|
+
/** Rendered while the PDF is being fetched, in place of children. */
|
|
28
|
+
pendingLabel?: React.ReactNode
|
|
29
|
+
children?: React.ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function PrintDocumentButton({
|
|
33
|
+
model,
|
|
34
|
+
id,
|
|
35
|
+
documentKey,
|
|
36
|
+
mode,
|
|
37
|
+
filename,
|
|
38
|
+
onError,
|
|
39
|
+
pendingLabel,
|
|
40
|
+
children,
|
|
41
|
+
disabled,
|
|
42
|
+
onClick,
|
|
43
|
+
...rest
|
|
44
|
+
}: PrintDocumentButtonProps) {
|
|
45
|
+
const printDocument = usePrintDocument()
|
|
46
|
+
const [busy, setBusy] = useState(false)
|
|
47
|
+
|
|
48
|
+
const handleClick = useCallback(
|
|
49
|
+
async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
50
|
+
onClick?.(e)
|
|
51
|
+
if (e.defaultPrevented) return
|
|
52
|
+
setBusy(true)
|
|
53
|
+
try {
|
|
54
|
+
await printDocument({ model, id, key: documentKey, mode, filename })
|
|
55
|
+
} catch (err) {
|
|
56
|
+
onError?.(err)
|
|
57
|
+
} finally {
|
|
58
|
+
setBusy(false)
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[printDocument, model, id, documentKey, mode, filename, onError, onClick],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
{...rest}
|
|
68
|
+
disabled={disabled || busy}
|
|
69
|
+
aria-busy={busy || undefined}
|
|
70
|
+
onClick={handleClick}
|
|
71
|
+
>
|
|
72
|
+
{busy && pendingLabel != null ? pendingLabel : children}
|
|
73
|
+
</button>
|
|
74
|
+
)
|
|
75
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -194,6 +194,15 @@ export interface RelationMeta {
|
|
|
194
194
|
readonly?: boolean
|
|
195
195
|
/** camelCase alias for `readonly`. */
|
|
196
196
|
readOnly?: boolean
|
|
197
|
+
/**
|
|
198
|
+
* Composition relation: its children are PART of the parent record (a
|
|
199
|
+
* document's lines), so the record MODAL embeds them as a sub-table.
|
|
200
|
+
* Opt-in — a modal renders only the relations carrying this flag, which is
|
|
201
|
+
* what keeps opening a warehouse from loading every stock movement it has.
|
|
202
|
+
* Served by the kernel from the manifest relation's `embed`. Absent (older
|
|
203
|
+
* kernels) = not embedded.
|
|
204
|
+
*/
|
|
205
|
+
embed?: boolean
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
export interface FilterDefinition {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// usePrintDocument — THE standard primitive for printing/downloading a
|
|
2
|
+
// server-rendered document (ticket, receipt, order) from any federated addon or
|
|
3
|
+
// host surface, without each addon reimplementing PDF fetching.
|
|
4
|
+
//
|
|
5
|
+
// The host (ops) renders documents declared in an addon's
|
|
6
|
+
// `contributions.documents[]` via a country/business-agnostic engine
|
|
7
|
+
// (pdf_chrome + document_render + org branding) and serves them at:
|
|
8
|
+
//
|
|
9
|
+
// GET /api/data/:model/:id/documents/:key.pdf → application/pdf
|
|
10
|
+
//
|
|
11
|
+
// The endpoint is auth-gated (Bearer), so we CANNOT just window.open the URL —
|
|
12
|
+
// that request carries no Authorization header and 401s. Instead we fetch the
|
|
13
|
+
// PDF through the injected ApiClient (which carries the token), turn the bytes
|
|
14
|
+
// into a blob URL, and print/download/open that. Re-printing is just calling
|
|
15
|
+
// this again — the render is an idempotent GET.
|
|
16
|
+
//
|
|
17
|
+
// The ApiClient is a PEER via <ApiProvider> (same one useAddonSettings uses), so
|
|
18
|
+
// this hook constructs no client of its own.
|
|
19
|
+
import { useCallback } from 'react'
|
|
20
|
+
import { useApi } from './api-context'
|
|
21
|
+
|
|
22
|
+
export interface PrintDocumentArgs {
|
|
23
|
+
/** The model KEY the document is declared against (e.g. "SalesOrder"). */
|
|
24
|
+
model: string
|
|
25
|
+
/** The record id. */
|
|
26
|
+
id: string
|
|
27
|
+
/** The document key from contributions.documents[].key (e.g. "sale_ticket"). */
|
|
28
|
+
key: string
|
|
29
|
+
/**
|
|
30
|
+
* print → open the PDF in a hidden iframe and fire the browser print dialog
|
|
31
|
+
* (default; best for thermal tickets — one click to the printer).
|
|
32
|
+
* download → save the PDF to disk.
|
|
33
|
+
* open → open the PDF in a new tab (user prints from the viewer).
|
|
34
|
+
*/
|
|
35
|
+
mode?: 'print' | 'download' | 'open'
|
|
36
|
+
/** Filename for the download mode (defaults to "<key>.pdf"). */
|
|
37
|
+
filename?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns a `printDocument(args)` callback. Resolves once the PDF has been
|
|
42
|
+
* fetched and the browser action (print/download/open) has been kicked off;
|
|
43
|
+
* rejects if the fetch fails (surface the error to a toast). Returns the blob
|
|
44
|
+
* URL created, revoked automatically after a minute.
|
|
45
|
+
*/
|
|
46
|
+
export function usePrintDocument() {
|
|
47
|
+
const api = useApi()
|
|
48
|
+
return useCallback(
|
|
49
|
+
async ({
|
|
50
|
+
model,
|
|
51
|
+
id,
|
|
52
|
+
key,
|
|
53
|
+
mode = 'print',
|
|
54
|
+
filename,
|
|
55
|
+
}: PrintDocumentArgs): Promise<string> => {
|
|
56
|
+
const url = `/data/${encodeURIComponent(model)}/${encodeURIComponent(
|
|
57
|
+
id,
|
|
58
|
+
)}/documents/${encodeURIComponent(key)}.pdf`
|
|
59
|
+
const res = await api.get(url, { responseType: 'blob' })
|
|
60
|
+
const blob =
|
|
61
|
+
res.data instanceof Blob
|
|
62
|
+
? res.data
|
|
63
|
+
: new Blob([res.data], { type: 'application/pdf' })
|
|
64
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
65
|
+
const cleanup = () => setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000)
|
|
66
|
+
|
|
67
|
+
if (mode === 'download') {
|
|
68
|
+
const a = document.createElement('a')
|
|
69
|
+
a.href = blobUrl
|
|
70
|
+
a.download = filename || `${key}.pdf`
|
|
71
|
+
document.body.appendChild(a)
|
|
72
|
+
a.click()
|
|
73
|
+
a.remove()
|
|
74
|
+
cleanup()
|
|
75
|
+
return blobUrl
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (mode === 'open') {
|
|
79
|
+
window.open(blobUrl, '_blank')
|
|
80
|
+
cleanup()
|
|
81
|
+
return blobUrl
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// mode === 'print': hidden iframe + contentWindow.print(). This is the
|
|
85
|
+
// reliable cross-browser way to auto-open the print dialog for a PDF
|
|
86
|
+
// blob (window.open + print() is blocked by the PDF viewer in Chrome).
|
|
87
|
+
const iframe = document.createElement('iframe')
|
|
88
|
+
iframe.style.position = 'fixed'
|
|
89
|
+
iframe.style.right = '0'
|
|
90
|
+
iframe.style.bottom = '0'
|
|
91
|
+
iframe.style.width = '0'
|
|
92
|
+
iframe.style.height = '0'
|
|
93
|
+
iframe.style.border = '0'
|
|
94
|
+
iframe.src = blobUrl
|
|
95
|
+
iframe.onload = () => {
|
|
96
|
+
try {
|
|
97
|
+
iframe.contentWindow?.focus()
|
|
98
|
+
iframe.contentWindow?.print()
|
|
99
|
+
} catch {
|
|
100
|
+
// Popup/print blocked — fall back to opening the PDF.
|
|
101
|
+
window.open(blobUrl, '_blank')
|
|
102
|
+
}
|
|
103
|
+
// Keep the iframe around long enough for the print dialog to read it.
|
|
104
|
+
setTimeout(() => iframe.remove(), 60_000)
|
|
105
|
+
cleanup()
|
|
106
|
+
}
|
|
107
|
+
document.body.appendChild(iframe)
|
|
108
|
+
return blobUrl
|
|
109
|
+
},
|
|
110
|
+
[api],
|
|
111
|
+
)
|
|
112
|
+
}
|