@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/action-modal-dispatcher.js +1 -1
  3. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  4. package/dist/dialogs/dynamic-record.js +1 -1
  5. package/dist/dynamic-columns.d.ts.map +1 -1
  6. package/dist/dynamic-columns.js +13 -1
  7. package/dist/dynamic-relation.d.ts +4 -0
  8. package/dist/dynamic-relation.d.ts.map +1 -1
  9. package/dist/dynamic-relation.js +80 -16
  10. package/dist/dynamic-relations.d.ts +22 -1
  11. package/dist/dynamic-relations.d.ts.map +1 -1
  12. package/dist/dynamic-relations.js +16 -3
  13. package/dist/entity-select.d.ts +43 -0
  14. package/dist/entity-select.d.ts.map +1 -0
  15. package/dist/entity-select.js +124 -0
  16. package/dist/index.d.ts +5 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -1
  19. package/dist/print-document-button.d.ts +21 -0
  20. package/dist/print-document-button.d.ts.map +1 -0
  21. package/dist/print-document-button.js +32 -0
  22. package/dist/types.d.ts +9 -0
  23. package/dist/types.d.ts.map +1 -1
  24. package/dist/use-print-document.d.ts +25 -0
  25. package/dist/use-print-document.d.ts.map +1 -0
  26. package/dist/use-print-document.js +79 -0
  27. package/package.json +3 -3
  28. package/src/__tests__/relation-embed-gate.test.tsx +110 -0
  29. package/src/action-modal-dispatcher.tsx +3 -1
  30. package/src/dialogs/dynamic-record.tsx +6 -1
  31. package/src/dynamic-columns.tsx +14 -1
  32. package/src/dynamic-relation.tsx +121 -23
  33. package/src/dynamic-relations.tsx +34 -2
  34. package/src/entity-select.tsx +316 -0
  35. package/src/index.ts +5 -0
  36. package/src/print-document-button.tsx +75 -0
  37. package/src/types.ts +9 -0
  38. 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
+ }