@asteby/metacore-runtime-react 28.7.0 → 28.8.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 +26 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +13 -1
- 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 +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- 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/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 +1 -1
- package/src/dynamic-columns.tsx +14 -1
- package/src/entity-select.tsx +316 -0
- package/src/index.ts +4 -0
- package/src/print-document-button.tsx +75 -0
- package/src/use-print-document.ts +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 28.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3db2705: Nuevo primitivo `EntitySelect`: combobox async de un modelo relacionado con las dos affordances del modal dinámico — "+" para CREAR (sin selección) que auto-selecciona el creado, y lápiz para EDITAR el seleccionado. Ambas gateadas por permisos del kernel (useCan: `<model>.create`/`.update`) y 100% dinámicas (el form sale de `/metadata/modal/:model` vía CreateRecordDialog). Reemplaza los pickers bespoke duplicados en POS/purchases por una sola implementación reusable.
|
|
8
|
+
- 8346d8d: PrintDocumentButton: botón reusable de impresión de documentos
|
|
9
|
+
|
|
10
|
+
Componente drop-in sobre usePrintDocument: renderiza un <button> (estilable con
|
|
11
|
+
className) que imprime/descarga/abre un documento del servidor, se deshabilita
|
|
12
|
+
mientras baja el PDF (aria-busy) y reporta errores por onError. Evita recablear
|
|
13
|
+
el hook + estado de carga en cada addon.
|
|
14
|
+
|
|
15
|
+
- 05bc470: usePrintDocument: primitivo reusable para imprimir documentos del servidor
|
|
16
|
+
|
|
17
|
+
Nuevo hook que cualquier addon federado usa para imprimir/descargar/abrir un
|
|
18
|
+
documento renderizado por el motor de docs de ops (ticket, comprobante, orden):
|
|
19
|
+
GET /api/data/:model/:id/documents/:key.pdf. Fetch con auth vía el ApiClient
|
|
20
|
+
inyectado (el endpoint es Bearer-gated, no se puede window.open directo), blob →
|
|
21
|
+
iframe print (modo default, ideal para ticket térmico), download u open. Era el
|
|
22
|
+
primitivo de impresión que faltaba en el SDK (había 0). Habilita el auto-print del
|
|
23
|
+
POS al cerrar venta.
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- 233d63b: DynamicTable: una celda `creator` sin actor resuelto (registro creado por el sistema, `created_by` null) muestra "Sistema" en vez de "N/A", consistente con el fallback del historial de actividad. `user`/`avatar`/`search` mantienen "N/A" (vacío = sin asignar).
|
|
28
|
+
|
|
3
29
|
## 28.7.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAwC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AAyCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAiB5D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OACqB,CAAA;AAwFhF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,GACzB,KAAK,gBAAgB,EACrB,KAAK,GAAG,EACR,OAAO,GAAG,EACV,mBAAe,KAChB,MAAM,GAAG,SAcX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA+ID;;;;GAIG;AACH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CACxC,CAqBA,CAAA;AAED,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+mBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -622,7 +622,19 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
622
622
|
col.tooltip ||
|
|
623
623
|
col.displayField ||
|
|
624
624
|
col.key;
|
|
625
|
-
|
|
625
|
+
// A `creator` cell with no resolved actor means the
|
|
626
|
+
// record was created by the SYSTEM (e.g. an owner /
|
|
627
|
+
// seed row whose `created_by` is null), not a missing
|
|
628
|
+
// value — show "Sistema" rather than "N/A". Matches
|
|
629
|
+
// the record-history actor fallback. `user`/`avatar`/
|
|
630
|
+
// `search` keep "N/A" (empty there means unassigned).
|
|
631
|
+
const resolvedName = getNestedValue(row.original, namePath);
|
|
632
|
+
const name = resolvedName ||
|
|
633
|
+
(renderAs === 'creator'
|
|
634
|
+
? t
|
|
635
|
+
? t('common.system', { defaultValue: 'Sistema' })
|
|
636
|
+
: 'Sistema'
|
|
637
|
+
: 'N/A');
|
|
626
638
|
const desc = getNestedValue(row.original, col.description || '');
|
|
627
639
|
const avatarSrc = resolveAvatarSrc(col, row.original, value, apiBaseUrl);
|
|
628
640
|
return (_jsx(AvatarCell, { name: String(name), desc: desc ? String(desc) : undefined, avatarSrc: avatarSrc, getImageUrl: getImageUrl }));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type LucideIcon } from 'lucide-react';
|
|
2
|
+
/** One searchable option: `value` is the id, `label` the display text. */
|
|
3
|
+
export interface EntitySelectOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EntitySelectProps {
|
|
9
|
+
/** Kernel model key (e.g. "Supplier", "Warehouse", "Category"). */
|
|
10
|
+
model: string;
|
|
11
|
+
/** Currently selected id (or null). */
|
|
12
|
+
value: string | null;
|
|
13
|
+
/** Label of the selected record (rendered without a re-fetch). */
|
|
14
|
+
label: string | null;
|
|
15
|
+
/** Called with (id, label) on select/create/clear. */
|
|
16
|
+
onSelect: (id: string | null, label: string | null) => void;
|
|
17
|
+
/** Async search over the model. Callers pass their `/api/options/<model>` fetcher. */
|
|
18
|
+
fetcher: (q: string, signal: AbortSignal) => Promise<EntitySelectOption[]>;
|
|
19
|
+
icon?: LucideIcon;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
searchPlaceholder?: string;
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
/** Preload first results on open and drop the 2-char gate. */
|
|
24
|
+
preload?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Permission overrides. By default create/edit are gated by the kernel
|
|
27
|
+
* permissions `<model>.create` / `<model>.update` (useCan). Pass explicit
|
|
28
|
+
* booleans to force them (e.g. a read-only surface).
|
|
29
|
+
*/
|
|
30
|
+
canCreate?: boolean;
|
|
31
|
+
canEdit?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* CRUD endpoint base for the create/edit dialog. Defaults to the standard
|
|
34
|
+
* org-scoped `/data/<model>/me`, with edit at `/data/<model>/me/<id>`.
|
|
35
|
+
*/
|
|
36
|
+
endpoint?: string;
|
|
37
|
+
/** Record field used as the label after create/edit (default "name"). */
|
|
38
|
+
labelField?: string;
|
|
39
|
+
/** Disable the whole control. */
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, }: EntitySelectProps): import("react").JSX.Element;
|
|
43
|
+
//# sourceMappingURL=entity-select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAiBvE,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAOD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EAAE,IAAI,EACV,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,GACnB,EAAE,iBAAiB,+BAuNnB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// EntitySelect — the shared, permission-aware single-select for a related model.
|
|
3
|
+
//
|
|
4
|
+
// A searchable async combobox over a kernel model's records, plus the two
|
|
5
|
+
// affordances every "pick a related record" control should have, exactly like
|
|
6
|
+
// the dynamic create modal's relation fields (Categoría/Marca) do:
|
|
7
|
+
//
|
|
8
|
+
// - nothing selected → a "+" that opens the model's CREATE dialog, and
|
|
9
|
+
// auto-selects the record it creates;
|
|
10
|
+
// - a record selected → a pencil that opens that record's EDIT dialog.
|
|
11
|
+
//
|
|
12
|
+
// Both affordances are gated by the kernel permissions (useCan): the "+" only
|
|
13
|
+
// shows when the user can create the model, the pencil only when they can edit
|
|
14
|
+
// it. Everything is DYNAMIC — the create/edit form comes from the model's
|
|
15
|
+
// `/metadata/modal/:model` schema via <CreateRecordDialog>, so no per-model form
|
|
16
|
+
// code is needed. This lives in the SDK so POS, purchases and any future addon
|
|
17
|
+
// share ONE implementation instead of each re-porting a bespoke picker.
|
|
18
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
19
|
+
import { Search, X, Plus, Pencil } from 'lucide-react';
|
|
20
|
+
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui';
|
|
21
|
+
import { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
22
|
+
import { useCan } from './permissions-context';
|
|
23
|
+
import { useApi } from './api-context';
|
|
24
|
+
/** Lowercase model → permission capability namespace (Supplier → supplier). */
|
|
25
|
+
function capabilityNamespace(model) {
|
|
26
|
+
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, }) {
|
|
29
|
+
const can = useCan();
|
|
30
|
+
const api = useApi();
|
|
31
|
+
const ns = capabilityNamespace(model);
|
|
32
|
+
const mayCreate = canCreate ?? can(`${ns}.create`);
|
|
33
|
+
const mayEdit = canEdit ?? can(`${ns}.update`);
|
|
34
|
+
const base = endpoint ?? `/data/${model}/me`;
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
37
|
+
const [dialogRecordId, setDialogRecordId] = useState(undefined);
|
|
38
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
39
|
+
const [results, setResults] = useState([]);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
41
|
+
const minChars = preload ? 0 : 2;
|
|
42
|
+
const run = useCallback(async (q, signal) => {
|
|
43
|
+
if (q.length < minChars) {
|
|
44
|
+
setResults([]);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
setIsLoading(true);
|
|
48
|
+
try {
|
|
49
|
+
const rows = await fetcher(q, signal);
|
|
50
|
+
if (!signal.aborted)
|
|
51
|
+
setResults(rows);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
if (!signal.aborted)
|
|
55
|
+
setResults([]);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (!signal.aborted)
|
|
59
|
+
setIsLoading(false);
|
|
60
|
+
}
|
|
61
|
+
}, [fetcher, minChars]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!open)
|
|
64
|
+
return;
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => run(searchTerm, controller.signal), 250);
|
|
67
|
+
return () => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
controller.abort();
|
|
70
|
+
};
|
|
71
|
+
}, [searchTerm, run, open]);
|
|
72
|
+
const pick = (row) => {
|
|
73
|
+
onSelect(row.value, row.label);
|
|
74
|
+
setOpen(false);
|
|
75
|
+
setSearchTerm('');
|
|
76
|
+
};
|
|
77
|
+
const clear = (e) => {
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
onSelect(null, null);
|
|
80
|
+
};
|
|
81
|
+
const openCreate = (e) => {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
setDialogRecordId(undefined);
|
|
84
|
+
setDialogOpen(true);
|
|
85
|
+
};
|
|
86
|
+
const openEdit = (e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (!value)
|
|
89
|
+
return;
|
|
90
|
+
setDialogRecordId(value);
|
|
91
|
+
setDialogOpen(true);
|
|
92
|
+
};
|
|
93
|
+
// Read the {id,label} off a saved record and select it. The transport
|
|
94
|
+
// matches the standard org-scoped CRUD the dynamic modal uses, so create/edit
|
|
95
|
+
// stay consistent with the rest of the app.
|
|
96
|
+
const selectSaved = (rec) => {
|
|
97
|
+
if (!rec)
|
|
98
|
+
return;
|
|
99
|
+
const id = rec.id != null ? String(rec.id) : value ?? '';
|
|
100
|
+
const lbl = (rec[labelField] != null && String(rec[labelField])) ||
|
|
101
|
+
(rec.name != null && String(rec.name)) ||
|
|
102
|
+
label ||
|
|
103
|
+
id;
|
|
104
|
+
onSelect(id, lbl);
|
|
105
|
+
};
|
|
106
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: disabled ? undefined : setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", disabled: disabled, className: "w-full flex-1 justify-start gap-2 font-normal", children: [Icon && _jsx(Icon, { className: "text-muted-foreground size-4 shrink-0" }), _jsx("span", { className: "flex-1 truncate text-left", children: label ?? placeholder }), value && (_jsx("span", { role: "button", tabIndex: 0, onClick: clear, onKeyDown: (e) => {
|
|
107
|
+
if (e.key === 'Enter' || e.key === ' ')
|
|
108
|
+
clear(e);
|
|
109
|
+
}, className: "hover:bg-accent ml-auto shrink-0 rounded-sm p-0.5", children: _jsx(X, { className: "size-3.5" }) }))] }) }), _jsx(PopoverContent, { className: "p-0", align: "start", style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: searchPlaceholder, value: searchTerm, onValueChange: setSearchTerm }), _jsxs(CommandList, { children: [isLoading && (_jsx("div", { className: "text-muted-foreground py-4 text-center text-sm", children: "Buscando\u2026" })), !isLoading &&
|
|
110
|
+
searchTerm.length >= minChars &&
|
|
111
|
+
results.length === 0 && _jsx(CommandEmpty, { children: emptyText }), !isLoading && results.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: results.map((row) => (_jsxs(CommandItem, { value: row.value, onSelect: () => pick(row), className: "flex flex-col items-start gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: row.label }), row.description && (_jsx("span", { className: "text-muted-foreground text-xs", children: row.description }))] }, row.value))) })), !isLoading && !preload && searchTerm.length < minChars && (_jsxs("div", { className: "text-muted-foreground flex flex-col items-center gap-1 py-6", children: [_jsx(Search, { className: "size-5" }), _jsx("span", { className: "text-xs", children: "Escribe al menos 2 caracteres" })] }))] })] }) })] }), value
|
|
112
|
+
? mayEdit && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openEdit, "aria-label": "Editar", title: "Editar", className: "shrink-0", children: _jsx(Pencil, { className: "size-4" }) }))
|
|
113
|
+
: mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openCreate, "aria-label": "Crear", title: "Crear", className: "shrink-0", children: _jsx(Plus, { className: "size-4" }) })), dialogOpen && (_jsx(CreateRecordDialog, { modelKey: model, open: dialogOpen, onOpenChange: setDialogOpen, recordId: dialogRecordId, endpoint: base, onCreate: async (data) => {
|
|
114
|
+
const res = await api.post(base, data);
|
|
115
|
+
const rec = (res.data?.data ?? res.data);
|
|
116
|
+
selectSaved(rec);
|
|
117
|
+
return rec.id != null ? { id: String(rec.id) } : undefined;
|
|
118
|
+
}, onUpdate: async (id, data) => {
|
|
119
|
+
const res = await api.put(`${base}/${id}`, data);
|
|
120
|
+
const rec = (res.data?.data ?? res.data);
|
|
121
|
+
selectSaved({ id, ...rec });
|
|
122
|
+
return { id: String(id) };
|
|
123
|
+
} }))] }));
|
|
124
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export * from './navigation-builder';
|
|
|
29
29
|
export * from './i18n-provider';
|
|
30
30
|
export * from './api-context';
|
|
31
31
|
export * from './use-addon-settings';
|
|
32
|
+
export * from './use-print-document';
|
|
33
|
+
export * from './print-document-button';
|
|
32
34
|
export * from './metadata-cache';
|
|
33
35
|
export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, type AddonManifestChangedMessage, type ManifestHotSwapClient, type WireHotSwapInvalidationOptions, } from './manifest-hotswap-subscriber';
|
|
34
36
|
export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, type HotSwapReloadStrategy, type HotSwapReloadConfig, type HotSwapReloadAction, type HotSwapReloadDeps, type UseHotSwapReloadResult, } from './hotswap-reload-policy';
|
|
@@ -43,6 +45,8 @@ export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
|
43
45
|
export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit';
|
|
44
46
|
export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record';
|
|
45
47
|
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
48
|
+
export { EntitySelect } from './entity-select';
|
|
49
|
+
export type { EntitySelectProps, EntitySelectOption } from './entity-select';
|
|
46
50
|
export { ViewRecordDialog } from './dialogs/view-record-dialog';
|
|
47
51
|
export type { ModelKey, ModelSchema, CreateResult, RecordDialogProps, CreateRecordDialogProps, ViewRecordDialogProps, } from './dialogs/types';
|
|
48
52
|
export { ExportDialog } from './dialogs/export';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,8 @@ export * from './navigation-builder';
|
|
|
34
34
|
export * from './i18n-provider';
|
|
35
35
|
export * from './api-context';
|
|
36
36
|
export * from './use-addon-settings';
|
|
37
|
+
export * from './use-print-document';
|
|
38
|
+
export * from './print-document-button';
|
|
37
39
|
export * from './metadata-cache';
|
|
38
40
|
export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, } from './manifest-hotswap-subscriber';
|
|
39
41
|
export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, } from './hotswap-reload-policy';
|
|
@@ -46,6 +48,7 @@ export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
|
46
48
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
47
49
|
export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit';
|
|
48
50
|
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
51
|
+
export { EntitySelect } from './entity-select';
|
|
49
52
|
export { ViewRecordDialog } from './dialogs/view-record-dialog';
|
|
50
53
|
export { ExportDialog } from './dialogs/export';
|
|
51
54
|
export { ImportDialog } from './dialogs/import';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type PrintDocumentArgs } from './use-print-document';
|
|
3
|
+
export interface PrintDocumentButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onError'> {
|
|
4
|
+
/** Model KEY the document is declared against (e.g. "SalesOrder"). */
|
|
5
|
+
model: string;
|
|
6
|
+
/** Record id. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Document key from contributions.documents[].key (e.g. "sale_ticket"). */
|
|
9
|
+
documentKey: string;
|
|
10
|
+
/** print (default) | download | open — see usePrintDocument. */
|
|
11
|
+
mode?: PrintDocumentArgs['mode'];
|
|
12
|
+
/** Download filename (mode="download"). */
|
|
13
|
+
filename?: string;
|
|
14
|
+
/** Called if the fetch/print fails (surface it to a toast in the host). */
|
|
15
|
+
onError?: (err: unknown) => void;
|
|
16
|
+
/** Rendered while the PDF is being fetched, in place of children. */
|
|
17
|
+
pendingLabel?: React.ReactNode;
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function PrintDocumentButton({ model, id, documentKey, mode, filename, onError, pendingLabel, children, disabled, onClick, ...rest }: PrintDocumentButtonProps): React.JSX.Element;
|
|
21
|
+
//# sourceMappingURL=print-document-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-document-button.d.ts","sourceRoot":"","sources":["../src/print-document-button.tsx"],"names":[],"mappings":"AASA,OAAO,KAAgC,MAAM,OAAO,CAAA;AACpD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAE/E,MAAM,WAAW,wBACb,SAAQ,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IACtE,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAChC,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,qEAAqE;IACrE,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,wBAAgB,mBAAmB,CAAC,EAChC,KAAK,EACL,EAAE,EACF,WAAW,EACX,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,GAAG,IAAI,EACV,EAAE,wBAAwB,qBA+B1B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// PrintDocumentButton — a drop-in button that prints/downloads a server-rendered
|
|
3
|
+
// document via usePrintDocument, so an addon doesn't re-wire the hook + loading
|
|
4
|
+
// state every time. Headless-friendly: it renders a plain <button> you style
|
|
5
|
+
// with `className`, disables itself while the PDF is fetching, and reports
|
|
6
|
+
// failures through `onError` (no toast dependency baked in).
|
|
7
|
+
//
|
|
8
|
+
// Example:
|
|
9
|
+
// <PrintDocumentButton model="SalesOrder" id={sale.id} documentKey="sale_ticket"
|
|
10
|
+
// className="btn">Imprimir ticket</PrintDocumentButton>
|
|
11
|
+
import { useCallback, useState } from 'react';
|
|
12
|
+
import { usePrintDocument } from './use-print-document';
|
|
13
|
+
export function PrintDocumentButton({ model, id, documentKey, mode, filename, onError, pendingLabel, children, disabled, onClick, ...rest }) {
|
|
14
|
+
const printDocument = usePrintDocument();
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const handleClick = useCallback(async (e) => {
|
|
17
|
+
onClick?.(e);
|
|
18
|
+
if (e.defaultPrevented)
|
|
19
|
+
return;
|
|
20
|
+
setBusy(true);
|
|
21
|
+
try {
|
|
22
|
+
await printDocument({ model, id, key: documentKey, mode, filename });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
onError?.(err);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
setBusy(false);
|
|
29
|
+
}
|
|
30
|
+
}, [printDocument, model, id, documentKey, mode, filename, onError, onClick]);
|
|
31
|
+
return (_jsx("button", { type: "button", ...rest, disabled: disabled || busy, "aria-busy": busy || undefined, onClick: handleClick, children: busy && pendingLabel != null ? pendingLabel : children }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface PrintDocumentArgs {
|
|
2
|
+
/** The model KEY the document is declared against (e.g. "SalesOrder"). */
|
|
3
|
+
model: string;
|
|
4
|
+
/** The record id. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The document key from contributions.documents[].key (e.g. "sale_ticket"). */
|
|
7
|
+
key: string;
|
|
8
|
+
/**
|
|
9
|
+
* print → open the PDF in a hidden iframe and fire the browser print dialog
|
|
10
|
+
* (default; best for thermal tickets — one click to the printer).
|
|
11
|
+
* download → save the PDF to disk.
|
|
12
|
+
* open → open the PDF in a new tab (user prints from the viewer).
|
|
13
|
+
*/
|
|
14
|
+
mode?: 'print' | 'download' | 'open';
|
|
15
|
+
/** Filename for the download mode (defaults to "<key>.pdf"). */
|
|
16
|
+
filename?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns a `printDocument(args)` callback. Resolves once the PDF has been
|
|
20
|
+
* fetched and the browser action (print/download/open) has been kicked off;
|
|
21
|
+
* rejects if the fetch fails (surface the error to a toast). Returns the blob
|
|
22
|
+
* URL created, revoked automatically after a minute.
|
|
23
|
+
*/
|
|
24
|
+
export declare function usePrintDocument(): ({ model, id, key, mode, filename, }: PrintDocumentArgs) => Promise<string>;
|
|
25
|
+
//# sourceMappingURL=use-print-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-print-document.d.ts","sourceRoot":"","sources":["../src/use-print-document.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,iBAAiB;IAC9B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,gFAAgF;IAChF,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAA;IACpC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,0CASrB,iBAAiB,KAAG,OAAO,CAAC,MAAM,CAAC,CAyD7C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
* Returns a `printDocument(args)` callback. Resolves once the PDF has been
|
|
23
|
+
* fetched and the browser action (print/download/open) has been kicked off;
|
|
24
|
+
* rejects if the fetch fails (surface the error to a toast). Returns the blob
|
|
25
|
+
* URL created, revoked automatically after a minute.
|
|
26
|
+
*/
|
|
27
|
+
export function usePrintDocument() {
|
|
28
|
+
const api = useApi();
|
|
29
|
+
return useCallback(async ({ model, id, key, mode = 'print', filename, }) => {
|
|
30
|
+
const url = `/data/${encodeURIComponent(model)}/${encodeURIComponent(id)}/documents/${encodeURIComponent(key)}.pdf`;
|
|
31
|
+
const res = await api.get(url, { responseType: 'blob' });
|
|
32
|
+
const blob = res.data instanceof Blob
|
|
33
|
+
? res.data
|
|
34
|
+
: new Blob([res.data], { type: 'application/pdf' });
|
|
35
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
36
|
+
const cleanup = () => setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
|
37
|
+
if (mode === 'download') {
|
|
38
|
+
const a = document.createElement('a');
|
|
39
|
+
a.href = blobUrl;
|
|
40
|
+
a.download = filename || `${key}.pdf`;
|
|
41
|
+
document.body.appendChild(a);
|
|
42
|
+
a.click();
|
|
43
|
+
a.remove();
|
|
44
|
+
cleanup();
|
|
45
|
+
return blobUrl;
|
|
46
|
+
}
|
|
47
|
+
if (mode === 'open') {
|
|
48
|
+
window.open(blobUrl, '_blank');
|
|
49
|
+
cleanup();
|
|
50
|
+
return blobUrl;
|
|
51
|
+
}
|
|
52
|
+
// mode === 'print': hidden iframe + contentWindow.print(). This is the
|
|
53
|
+
// reliable cross-browser way to auto-open the print dialog for a PDF
|
|
54
|
+
// blob (window.open + print() is blocked by the PDF viewer in Chrome).
|
|
55
|
+
const iframe = document.createElement('iframe');
|
|
56
|
+
iframe.style.position = 'fixed';
|
|
57
|
+
iframe.style.right = '0';
|
|
58
|
+
iframe.style.bottom = '0';
|
|
59
|
+
iframe.style.width = '0';
|
|
60
|
+
iframe.style.height = '0';
|
|
61
|
+
iframe.style.border = '0';
|
|
62
|
+
iframe.src = blobUrl;
|
|
63
|
+
iframe.onload = () => {
|
|
64
|
+
try {
|
|
65
|
+
iframe.contentWindow?.focus();
|
|
66
|
+
iframe.contentWindow?.print();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Popup/print blocked — fall back to opening the PDF.
|
|
70
|
+
window.open(blobUrl, '_blank');
|
|
71
|
+
}
|
|
72
|
+
// Keep the iframe around long enough for the print dialog to read it.
|
|
73
|
+
setTimeout(() => iframe.remove(), 60_000);
|
|
74
|
+
cleanup();
|
|
75
|
+
};
|
|
76
|
+
document.body.appendChild(iframe);
|
|
77
|
+
return blobUrl;
|
|
78
|
+
}, [api]);
|
|
79
|
+
}
|
package/package.json
CHANGED
package/src/dynamic-columns.tsx
CHANGED
|
@@ -899,7 +899,20 @@ export function makeDefaultGetDynamicColumns(
|
|
|
899
899
|
col.tooltip ||
|
|
900
900
|
col.displayField ||
|
|
901
901
|
col.key
|
|
902
|
-
|
|
902
|
+
// A `creator` cell with no resolved actor means the
|
|
903
|
+
// record was created by the SYSTEM (e.g. an owner /
|
|
904
|
+
// seed row whose `created_by` is null), not a missing
|
|
905
|
+
// value — show "Sistema" rather than "N/A". Matches
|
|
906
|
+
// the record-history actor fallback. `user`/`avatar`/
|
|
907
|
+
// `search` keep "N/A" (empty there means unassigned).
|
|
908
|
+
const resolvedName = getNestedValue(row.original, namePath)
|
|
909
|
+
const name =
|
|
910
|
+
resolvedName ||
|
|
911
|
+
(renderAs === 'creator'
|
|
912
|
+
? t
|
|
913
|
+
? t('common.system', { defaultValue: 'Sistema' })
|
|
914
|
+
: 'Sistema'
|
|
915
|
+
: 'N/A')
|
|
903
916
|
const desc = getNestedValue(row.original, col.description || '')
|
|
904
917
|
|
|
905
918
|
const avatarSrc = resolveAvatarSrc(col, row.original, value, apiBaseUrl)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// EntitySelect — the shared, permission-aware single-select for a related model.
|
|
2
|
+
//
|
|
3
|
+
// A searchable async combobox over a kernel model's records, plus the two
|
|
4
|
+
// affordances every "pick a related record" control should have, exactly like
|
|
5
|
+
// the dynamic create modal's relation fields (Categoría/Marca) do:
|
|
6
|
+
//
|
|
7
|
+
// - nothing selected → a "+" that opens the model's CREATE dialog, and
|
|
8
|
+
// auto-selects the record it creates;
|
|
9
|
+
// - a record selected → a pencil that opens that record's EDIT dialog.
|
|
10
|
+
//
|
|
11
|
+
// Both affordances are gated by the kernel permissions (useCan): the "+" only
|
|
12
|
+
// shows when the user can create the model, the pencil only when they can edit
|
|
13
|
+
// it. Everything is DYNAMIC — the create/edit form comes from the model's
|
|
14
|
+
// `/metadata/modal/:model` schema via <CreateRecordDialog>, so no per-model form
|
|
15
|
+
// code is needed. This lives in the SDK so POS, purchases and any future addon
|
|
16
|
+
// share ONE implementation instead of each re-porting a bespoke picker.
|
|
17
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
18
|
+
import { Search, X, Plus, Pencil, type LucideIcon } from 'lucide-react'
|
|
19
|
+
import {
|
|
20
|
+
Button,
|
|
21
|
+
Command,
|
|
22
|
+
CommandEmpty,
|
|
23
|
+
CommandGroup,
|
|
24
|
+
CommandInput,
|
|
25
|
+
CommandItem,
|
|
26
|
+
CommandList,
|
|
27
|
+
Popover,
|
|
28
|
+
PopoverContent,
|
|
29
|
+
PopoverTrigger,
|
|
30
|
+
} from '@asteby/metacore-ui'
|
|
31
|
+
import { CreateRecordDialog } from './dialogs/create-record-dialog'
|
|
32
|
+
import { useCan } from './permissions-context'
|
|
33
|
+
import { useApi } from './api-context'
|
|
34
|
+
|
|
35
|
+
/** One searchable option: `value` is the id, `label` the display text. */
|
|
36
|
+
export interface EntitySelectOption {
|
|
37
|
+
value: string
|
|
38
|
+
label: string
|
|
39
|
+
description?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EntitySelectProps {
|
|
43
|
+
/** Kernel model key (e.g. "Supplier", "Warehouse", "Category"). */
|
|
44
|
+
model: string
|
|
45
|
+
/** Currently selected id (or null). */
|
|
46
|
+
value: string | null
|
|
47
|
+
/** Label of the selected record (rendered without a re-fetch). */
|
|
48
|
+
label: string | null
|
|
49
|
+
/** Called with (id, label) on select/create/clear. */
|
|
50
|
+
onSelect: (id: string | null, label: string | null) => void
|
|
51
|
+
/** Async search over the model. Callers pass their `/api/options/<model>` fetcher. */
|
|
52
|
+
fetcher: (q: string, signal: AbortSignal) => Promise<EntitySelectOption[]>
|
|
53
|
+
|
|
54
|
+
icon?: LucideIcon
|
|
55
|
+
placeholder?: string
|
|
56
|
+
searchPlaceholder?: string
|
|
57
|
+
emptyText?: string
|
|
58
|
+
/** Preload first results on open and drop the 2-char gate. */
|
|
59
|
+
preload?: boolean
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Permission overrides. By default create/edit are gated by the kernel
|
|
63
|
+
* permissions `<model>.create` / `<model>.update` (useCan). Pass explicit
|
|
64
|
+
* booleans to force them (e.g. a read-only surface).
|
|
65
|
+
*/
|
|
66
|
+
canCreate?: boolean
|
|
67
|
+
canEdit?: boolean
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* CRUD endpoint base for the create/edit dialog. Defaults to the standard
|
|
71
|
+
* org-scoped `/data/<model>/me`, with edit at `/data/<model>/me/<id>`.
|
|
72
|
+
*/
|
|
73
|
+
endpoint?: string
|
|
74
|
+
/** Record field used as the label after create/edit (default "name"). */
|
|
75
|
+
labelField?: string
|
|
76
|
+
/** Disable the whole control. */
|
|
77
|
+
disabled?: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Lowercase model → permission capability namespace (Supplier → supplier). */
|
|
81
|
+
function capabilityNamespace(model: string): string {
|
|
82
|
+
return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function EntitySelect({
|
|
86
|
+
model,
|
|
87
|
+
value,
|
|
88
|
+
label,
|
|
89
|
+
onSelect,
|
|
90
|
+
fetcher,
|
|
91
|
+
icon: Icon,
|
|
92
|
+
placeholder = 'Seleccionar…',
|
|
93
|
+
searchPlaceholder = 'Buscar…',
|
|
94
|
+
emptyText = 'Sin resultados',
|
|
95
|
+
preload = false,
|
|
96
|
+
canCreate,
|
|
97
|
+
canEdit,
|
|
98
|
+
endpoint,
|
|
99
|
+
labelField = 'name',
|
|
100
|
+
disabled = false,
|
|
101
|
+
}: EntitySelectProps) {
|
|
102
|
+
const can = useCan()
|
|
103
|
+
const api = useApi()
|
|
104
|
+
|
|
105
|
+
const ns = capabilityNamespace(model)
|
|
106
|
+
const mayCreate = canCreate ?? can(`${ns}.create`)
|
|
107
|
+
const mayEdit = canEdit ?? can(`${ns}.update`)
|
|
108
|
+
const base = endpoint ?? `/data/${model}/me`
|
|
109
|
+
|
|
110
|
+
const [open, setOpen] = useState(false)
|
|
111
|
+
const [dialogOpen, setDialogOpen] = useState(false)
|
|
112
|
+
const [dialogRecordId, setDialogRecordId] = useState<string | undefined>(undefined)
|
|
113
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
114
|
+
const [results, setResults] = useState<EntitySelectOption[]>([])
|
|
115
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
116
|
+
|
|
117
|
+
const minChars = preload ? 0 : 2
|
|
118
|
+
|
|
119
|
+
const run = useCallback(
|
|
120
|
+
async (q: string, signal: AbortSignal) => {
|
|
121
|
+
if (q.length < minChars) {
|
|
122
|
+
setResults([])
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
setIsLoading(true)
|
|
126
|
+
try {
|
|
127
|
+
const rows = await fetcher(q, signal)
|
|
128
|
+
if (!signal.aborted) setResults(rows)
|
|
129
|
+
} catch {
|
|
130
|
+
if (!signal.aborted) setResults([])
|
|
131
|
+
} finally {
|
|
132
|
+
if (!signal.aborted) setIsLoading(false)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
[fetcher, minChars],
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!open) return
|
|
140
|
+
const controller = new AbortController()
|
|
141
|
+
const timeout = setTimeout(() => run(searchTerm, controller.signal), 250)
|
|
142
|
+
return () => {
|
|
143
|
+
clearTimeout(timeout)
|
|
144
|
+
controller.abort()
|
|
145
|
+
}
|
|
146
|
+
}, [searchTerm, run, open])
|
|
147
|
+
|
|
148
|
+
const pick = (row: EntitySelectOption) => {
|
|
149
|
+
onSelect(row.value, row.label)
|
|
150
|
+
setOpen(false)
|
|
151
|
+
setSearchTerm('')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const clear = (e: React.MouseEvent) => {
|
|
155
|
+
e.stopPropagation()
|
|
156
|
+
onSelect(null, null)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const openCreate = (e: React.MouseEvent) => {
|
|
160
|
+
e.stopPropagation()
|
|
161
|
+
setDialogRecordId(undefined)
|
|
162
|
+
setDialogOpen(true)
|
|
163
|
+
}
|
|
164
|
+
const openEdit = (e: React.MouseEvent) => {
|
|
165
|
+
e.stopPropagation()
|
|
166
|
+
if (!value) return
|
|
167
|
+
setDialogRecordId(value)
|
|
168
|
+
setDialogOpen(true)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Read the {id,label} off a saved record and select it. The transport
|
|
172
|
+
// matches the standard org-scoped CRUD the dynamic modal uses, so create/edit
|
|
173
|
+
// stay consistent with the rest of the app.
|
|
174
|
+
const selectSaved = (rec: Record<string, unknown> | undefined | null) => {
|
|
175
|
+
if (!rec) return
|
|
176
|
+
const id = rec.id != null ? String(rec.id) : value ?? ''
|
|
177
|
+
const lbl =
|
|
178
|
+
(rec[labelField] != null && String(rec[labelField])) ||
|
|
179
|
+
(rec.name != null && String(rec.name)) ||
|
|
180
|
+
label ||
|
|
181
|
+
id
|
|
182
|
+
onSelect(id, lbl)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex items-center gap-1.5">
|
|
187
|
+
<Popover open={open} onOpenChange={disabled ? undefined : setOpen}>
|
|
188
|
+
<PopoverTrigger asChild>
|
|
189
|
+
<Button
|
|
190
|
+
variant="outline"
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
className="w-full flex-1 justify-start gap-2 font-normal"
|
|
193
|
+
>
|
|
194
|
+
{Icon && <Icon className="text-muted-foreground size-4 shrink-0" />}
|
|
195
|
+
<span className="flex-1 truncate text-left">{label ?? placeholder}</span>
|
|
196
|
+
{value && (
|
|
197
|
+
<span
|
|
198
|
+
role="button"
|
|
199
|
+
tabIndex={0}
|
|
200
|
+
onClick={clear}
|
|
201
|
+
onKeyDown={(e) => {
|
|
202
|
+
if (e.key === 'Enter' || e.key === ' ')
|
|
203
|
+
clear(e as unknown as React.MouseEvent)
|
|
204
|
+
}}
|
|
205
|
+
className="hover:bg-accent ml-auto shrink-0 rounded-sm p-0.5"
|
|
206
|
+
>
|
|
207
|
+
<X className="size-3.5" />
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
210
|
+
</Button>
|
|
211
|
+
</PopoverTrigger>
|
|
212
|
+
<PopoverContent
|
|
213
|
+
className="p-0"
|
|
214
|
+
align="start"
|
|
215
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
216
|
+
>
|
|
217
|
+
<Command shouldFilter={false}>
|
|
218
|
+
<CommandInput
|
|
219
|
+
placeholder={searchPlaceholder}
|
|
220
|
+
value={searchTerm}
|
|
221
|
+
onValueChange={setSearchTerm}
|
|
222
|
+
/>
|
|
223
|
+
<CommandList>
|
|
224
|
+
{isLoading && (
|
|
225
|
+
<div className="text-muted-foreground py-4 text-center text-sm">
|
|
226
|
+
Buscando…
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
{!isLoading &&
|
|
230
|
+
searchTerm.length >= minChars &&
|
|
231
|
+
results.length === 0 && <CommandEmpty>{emptyText}</CommandEmpty>}
|
|
232
|
+
{!isLoading && results.length > 0 && (
|
|
233
|
+
<CommandGroup className="max-h-64 overflow-auto">
|
|
234
|
+
{results.map((row) => (
|
|
235
|
+
<CommandItem
|
|
236
|
+
key={row.value}
|
|
237
|
+
value={row.value}
|
|
238
|
+
onSelect={() => pick(row)}
|
|
239
|
+
className="flex flex-col items-start gap-0.5"
|
|
240
|
+
>
|
|
241
|
+
<span className="text-sm font-medium">{row.label}</span>
|
|
242
|
+
{row.description && (
|
|
243
|
+
<span className="text-muted-foreground text-xs">
|
|
244
|
+
{row.description}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</CommandItem>
|
|
248
|
+
))}
|
|
249
|
+
</CommandGroup>
|
|
250
|
+
)}
|
|
251
|
+
{!isLoading && !preload && searchTerm.length < minChars && (
|
|
252
|
+
<div className="text-muted-foreground flex flex-col items-center gap-1 py-6">
|
|
253
|
+
<Search className="size-5" />
|
|
254
|
+
<span className="text-xs">Escribe al menos 2 caracteres</span>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</CommandList>
|
|
258
|
+
</Command>
|
|
259
|
+
</PopoverContent>
|
|
260
|
+
</Popover>
|
|
261
|
+
|
|
262
|
+
{/* Selected → edit (pencil); empty → create (+). Each gated by perms. */}
|
|
263
|
+
{value
|
|
264
|
+
? mayEdit && (
|
|
265
|
+
<Button
|
|
266
|
+
type="button"
|
|
267
|
+
variant="outline"
|
|
268
|
+
size="icon"
|
|
269
|
+
disabled={disabled}
|
|
270
|
+
onClick={openEdit}
|
|
271
|
+
aria-label="Editar"
|
|
272
|
+
title="Editar"
|
|
273
|
+
className="shrink-0"
|
|
274
|
+
>
|
|
275
|
+
<Pencil className="size-4" />
|
|
276
|
+
</Button>
|
|
277
|
+
)
|
|
278
|
+
: mayCreate && (
|
|
279
|
+
<Button
|
|
280
|
+
type="button"
|
|
281
|
+
variant="outline"
|
|
282
|
+
size="icon"
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
onClick={openCreate}
|
|
285
|
+
aria-label="Crear"
|
|
286
|
+
title="Crear"
|
|
287
|
+
className="shrink-0"
|
|
288
|
+
>
|
|
289
|
+
<Plus className="size-4" />
|
|
290
|
+
</Button>
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{dialogOpen && (
|
|
294
|
+
<CreateRecordDialog
|
|
295
|
+
modelKey={model}
|
|
296
|
+
open={dialogOpen}
|
|
297
|
+
onOpenChange={setDialogOpen}
|
|
298
|
+
recordId={dialogRecordId}
|
|
299
|
+
endpoint={base}
|
|
300
|
+
onCreate={async (data) => {
|
|
301
|
+
const res = await api.post(base, data)
|
|
302
|
+
const rec = (res.data?.data ?? res.data) as Record<string, unknown>
|
|
303
|
+
selectSaved(rec)
|
|
304
|
+
return rec.id != null ? { id: String(rec.id) } : undefined
|
|
305
|
+
}}
|
|
306
|
+
onUpdate={async (id, data) => {
|
|
307
|
+
const res = await api.put(`${base}/${id}`, data)
|
|
308
|
+
const rec = (res.data?.data ?? res.data) as Record<string, unknown>
|
|
309
|
+
selectSaved({ id, ...rec })
|
|
310
|
+
return { id: String(id) }
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -175,6 +175,8 @@ export * from './navigation-builder'
|
|
|
175
175
|
export * from './i18n-provider'
|
|
176
176
|
export * from './api-context'
|
|
177
177
|
export * from './use-addon-settings'
|
|
178
|
+
export * from './use-print-document'
|
|
179
|
+
export * from './print-document-button'
|
|
178
180
|
export * from './metadata-cache'
|
|
179
181
|
export {
|
|
180
182
|
ADDON_MANIFEST_CHANGED_TYPE,
|
|
@@ -242,6 +244,8 @@ export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
|
|
|
242
244
|
export { normalizeRefFieldsForSubmit } from './dialogs/normalize-submit'
|
|
243
245
|
export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'
|
|
244
246
|
export { CreateRecordDialog } from './dialogs/create-record-dialog'
|
|
247
|
+
export { EntitySelect } from './entity-select'
|
|
248
|
+
export type { EntitySelectProps, EntitySelectOption } from './entity-select'
|
|
245
249
|
export { ViewRecordDialog } from './dialogs/view-record-dialog'
|
|
246
250
|
export type {
|
|
247
251
|
ModelKey,
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|