@asteby/metacore-runtime-react 46.3.0 → 47.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 +31 -0
- package/dist/action-modal-dispatcher.d.ts +10 -1
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +107 -19
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +24 -0
- package/dist/business/index.d.ts +3 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +3 -0
- package/dist/business/print-send-dialog.d.ts +27 -0
- package/dist/business/print-send-dialog.d.ts.map +1 -0
- package/dist/business/print-send-dialog.js +55 -0
- package/dist/business/relate-documents.d.ts +41 -0
- package/dist/business/relate-documents.d.ts.map +1 -0
- package/dist/business/relate-documents.js +42 -0
- package/dist/business/vehicle-picker.d.ts +36 -0
- package/dist/business/vehicle-picker.d.ts.map +1 -0
- package/dist/business/vehicle-picker.js +61 -0
- package/dist/cfdi-stamp-panel.d.ts +29 -0
- package/dist/cfdi-stamp-panel.d.ts.map +1 -0
- package/dist/cfdi-stamp-panel.js +53 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +5 -0
- package/dist/document-form-dialog.d.ts +23 -0
- package/dist/document-form-dialog.d.ts.map +1 -0
- package/dist/document-form-dialog.js +196 -0
- package/dist/dynamic-crud-page.d.ts +6 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +17 -7
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +3 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +6 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/list-primary-action.d.ts +31 -0
- package/dist/list-primary-action.d.ts.map +1 -0
- package/dist/list-primary-action.js +35 -0
- package/dist/model-action-toolbar.d.ts +7 -1
- package/dist/model-action-toolbar.d.ts.map +1 -1
- package/dist/model-action-toolbar.js +10 -3
- package/dist/model-extension-registry.d.ts +7 -0
- package/dist/model-extension-registry.d.ts.map +1 -1
- package/dist/nav-permissions.d.ts +19 -0
- package/dist/nav-permissions.d.ts.map +1 -0
- package/dist/nav-permissions.js +51 -0
- package/dist/navigation-builder.d.ts +6 -0
- package/dist/navigation-builder.d.ts.map +1 -1
- package/dist/navigation-builder.js +26 -1
- package/dist/permissions-manager.d.ts +9 -0
- package/dist/permissions-manager.d.ts.map +1 -1
- package/dist/permissions-manager.js +50 -17
- package/dist/print-document-button.d.ts.map +1 -1
- package/dist/print-document-button.js +9 -1
- package/dist/record-mutation-events.d.ts +14 -0
- package/dist/record-mutation-events.d.ts.map +1 -0
- package/dist/record-mutation-events.js +37 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/use-print-document.d.ts +15 -1
- package/dist/use-print-document.d.ts.map +1 -1
- package/dist/use-print-document.js +52 -1
- package/dist/use-record-mutation-tick.d.ts +6 -0
- package/dist/use-record-mutation-tick.d.ts.map +1 -0
- package/dist/use-record-mutation-tick.js +11 -0
- package/package.json +3 -3
- package/src/__tests__/action-modal-feedback.test.tsx +116 -0
- package/src/__tests__/business-pickers.test.tsx +157 -0
- package/src/__tests__/document-form-dialog.test.tsx +190 -0
- package/src/__tests__/dynamic-table-mutation-refresh.test.tsx +116 -0
- package/src/__tests__/list-primary-action.test.tsx +157 -0
- package/src/__tests__/nav-permissions.test.tsx +76 -0
- package/src/__tests__/permissions-manager.test.tsx +21 -4
- package/src/__tests__/use-print-document-feedback.test.tsx +58 -0
- package/src/action-modal-dispatcher.tsx +124 -6
- package/src/business/catalog.ts +24 -0
- package/src/business/index.ts +3 -0
- package/src/business/print-send-dialog.tsx +175 -0
- package/src/business/relate-documents.tsx +182 -0
- package/src/business/vehicle-picker.tsx +188 -0
- package/src/cfdi-stamp-panel.tsx +146 -0
- package/src/dialogs/dynamic-record.tsx +5 -0
- package/src/document-form-dialog.tsx +357 -0
- package/src/dynamic-crud-page.tsx +36 -7
- package/src/dynamic-kanban.tsx +3 -1
- package/src/dynamic-table.tsx +6 -2
- package/src/index.ts +36 -0
- package/src/list-primary-action.ts +71 -0
- package/src/model-action-toolbar.tsx +18 -3
- package/src/model-extension-registry.tsx +7 -0
- package/src/nav-permissions.ts +58 -0
- package/src/navigation-builder.tsx +27 -1
- package/src/permissions-manager.tsx +39 -26
- package/src/print-document-button.tsx +9 -1
- package/src/record-mutation-events.ts +50 -0
- package/src/types.ts +63 -0
- package/src/use-print-document.ts +66 -3
- package/src/use-record-mutation-tick.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 47.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4371f2c: Diálogos de acción: resultado de timbrado y errores del servidor.
|
|
8
|
+
|
|
9
|
+
- #1024 (FAC-3 / PIT-051): nuevo `CfdiStampPanel` / `CfdiStampResultDialog` / `extractStampResult`. Cuando la acción responde con `fiscal_uuid`, `pdf_url` y/o `xml_url`, el diálogo muestra el UUID como texto y enlaces «Descargar PDF» / «Descargar XML» en lugar del JSON. Sin esas claves no se inventa un resultado fiscal.
|
|
10
|
+
- #1023 (PIT-046): los diálogos de acción (genérico, wizard y confirmación) pintan un `FormErrorBanner` dentro del diálogo con el 422 (`errors` por campo) o el mensaje y causa de un 500, y permanecen abiertos.
|
|
11
|
+
|
|
12
|
+
- 4371f2c: FAC-12 (#1022): alta guiada por tipo de documento. Nuevo `DocumentFormDialog` y `resolveDocumentForms`: cuando el metadata de la tabla (o la prop `documentForms` de `DynamicCRUDPage`) trae `document_forms`, «Crear» muestra tarjetas de tipo (Factura / Nota de crédito / REP / Global / Traslado…) y, al elegir, los campos de ese tipo y un paso de renglones con `DocumentLinesGrid`; un solo tipo se salta el selector. Escribe el valor del tipo en `type_field`, serializa los renglones con `serializeLineItems`, muestra los 422 (banner + campos) dentro del diálogo y emite el evento de mutación para refrescar la lista. Sin manifest, el alta genérica no cambia. El SDK no embebe catálogos SAT: campos y valores los sirve el kernel o el addon. Nuevos tipos: `DocumentFormsManifest`, `DocumentFormType`, `DocumentFormLines`, `TableMetadata.document_forms`.
|
|
13
|
+
- 4371f2c: PIT-044: el menú lateral se filtra por los permisos efectivos del rol. `@asteby/metacore-ui` agrega `filterNavGroups`, `NavLinkItem.requires` y la prop `isItemVisible` de `AppSidebar`; `@asteby/metacore-runtime-react` agrega `useNavItemVisible`, `isNavItemAllowed`, `capabilityForNavItem` (una entrada `/m/<modelo>` exige `<modelo>.index`; las demás rutas no se bloquean) y `useNavigation` respeta `NavItem.requires`; el `AppSidebar` de starter-core lo aplica. Sin `PermissionsProvider` (o con `isAdmin`) no se oculta nada.
|
|
14
|
+
- 4371f2c: Las listas se refrescan tras crear, editar o borrar (#1020). `DynamicRecordDialog` y `ActionModalDispatcher` (componente federado, wizard, formulario genérico y confirmación) emiten el evento `metacore:record-mutated` con el modelo; `DynamicTable` y `DynamicKanban` vuelven a pedir los datos cuando el modelo coincide (sin distinguir mayúsculas). Nuevos exports: `emitRecordMutation`, `subscribeRecordMutations`, `useRecordMutationTick`.
|
|
15
|
+
- 4371f2c: Tres componentes de negocio compartidos:
|
|
16
|
+
|
|
17
|
+
- `VehiclePicker` (#1025): selector controlado de vehículo con búsqueda por placa o VIN y alta rápida (`CreateRecordDialog`), mismo estilo que `CustomerPicker`; sin `PermissionsProvider` no bloquea la UI.
|
|
18
|
+
- `RelateDocuments` (#1026): relaciona uno o más documentos origen con un tipo de relación (`related_document_id`, `relation_type`), muestra UUID/folio; los tipos llegan por prop (sin catálogos fiscales embebidos).
|
|
19
|
+
- `PrintSendDialog` (#1027): imprimir (URL de PDF del host) o enviar por `onSend(canal, destino, documento)`; sin integración de mensajería.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- 4371f2c: Roles y permisos (#1021): «Permisos generales» deja de ser una columna de tarjetas junto al rol y pasa a ser un grupo sintético del combobox de módulos; al elegirlo se editan en la misma grilla de dos columnas que las acciones de un módulo, y el conteo de concedidos los incluye. Nuevo `generalPermissionsModule`.
|
|
24
|
+
- 4371f2c: Auditoría Ronda D: «Descargar Factura CFDI» no daba ninguna señal. `usePrintDocument` ahora muestra un toast de progreso, uno de éxito y, si falla, el motivo que responde el servidor (también cuando el cuerpo llega como Blob); la promesa sigue rechazando. `feedback: false` lo silencia y `PrintDocumentButton` deja el toast de error al llamador cuando pasa `onError`.
|
|
25
|
+
- Updated dependencies [4371f2c]
|
|
26
|
+
- @asteby/metacore-ui@2.24.0
|
|
27
|
+
|
|
28
|
+
## 46.4.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- e47bca8: Una sola acción primaria de alta por pantalla. `resolveListPrimaryAction` oculta el Crear genérico cuando hay `placement: create`, `replaces_create`, `create_mode: hidden|action`, `canCreate: false` o `ModelExtension.primaryActionKey`. Si dos acciones reclaman el alta, solo una lleva `data-primary`.
|
|
33
|
+
|
|
3
34
|
## 46.3.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Translate } from './server-error';
|
|
1
2
|
import type { ActionFieldDef } from './types';
|
|
2
3
|
import { type ActionMetadata, type ActionModalProps } from '@asteby/metacore-sdk';
|
|
3
4
|
export type { ActionMetadata, ActionModalProps };
|
|
@@ -66,7 +67,15 @@ export declare function unwrapRecordScalar(value: unknown): unknown;
|
|
|
66
67
|
export declare function readRecordPath(record: any, path: string): unknown;
|
|
67
68
|
/** Scalar seed for one action field from the row being acted on. */
|
|
68
69
|
export declare function scalarDefaultFromRecord(field: ActionFieldDef, record: any): unknown;
|
|
69
|
-
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }: ActionModalProps): import("react").JSX.Element | null;
|
|
70
|
+
export declare function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }: ActionModalProps): import("react").JSX.Element | null;
|
|
70
71
|
/** How long an action with `modal` waits for its federated component. */
|
|
71
72
|
export declare const FEDERATED_ACTION_MODAL_TIMEOUT_MS = 20000;
|
|
73
|
+
/** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
|
|
74
|
+
* 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
|
|
75
|
+
* other failure (500, network) -> the server's own message and cause. Unlike
|
|
76
|
+
* the toast it stays on screen until the next attempt. */
|
|
77
|
+
export declare function actionErrorBannerMessage(err: unknown, fields: readonly ActionFieldDef[] | undefined, t: Translate, language?: string): string;
|
|
78
|
+
export declare function buildFieldDefaults(fields: ActionFieldDef[], record: any): Record<string, any>;
|
|
79
|
+
/** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
|
|
80
|
+
export declare function renderField(field: ActionFieldDef, value: any, onChange: (value: any) => void, formValues?: Record<string, any>, record?: Record<string, any>, fieldErrors?: Record<string, string>): import("react").JSX.Element;
|
|
72
81
|
//# sourceMappingURL=action-modal-dispatcher.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AA8B/C,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAGxB,MAAM,sBAAsB,CAAA;AAG7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAYD,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAC1C;AAED,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,MAAM,EAAE,GAAG,EACX,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H,sBAAsB,EAAE,CAyB1B;AASD,MAAM,WAAW,uBAAuB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,wBAAwB,CACpC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC9B,uBAAuB,EAAE,CAe3B;AAKD,wBAAgB,+BAA+B,CAC3C,GAAG,EAAE,uBAAuB,EAC5B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CActE;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,EAAE,aAAa,GAC3B,EAAE,gBAAgB,sCA0FlB;AAED,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,QAAS,CAAA;AA6VvD;;;2DAG2D;AAC3D,wBAAgB,wBAAwB,CACpC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAC7C,CAAC,EAAE,SAAS,EACZ,QAAQ,CAAC,EAAE,MAAM,GAClB,MAAM,CAWR;AA+bD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAoB7F;AAyPD,oGAAoG;AACpG,wBAAgB,WAAW,CACvB,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,GAAG,EACV,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,EAK9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,+BAwEvC"}
|
|
@@ -15,13 +15,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
//
|
|
16
16
|
// The host injects its axios-like client via <ApiProvider>; we no longer
|
|
17
17
|
// depend on a bundler alias to `@/lib/api`.
|
|
18
|
-
import {
|
|
18
|
+
import { emitRecordMutation } from './record-mutation-events';
|
|
19
|
+
import { useState, useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
19
20
|
import { useTranslation } from 'react-i18next';
|
|
20
21
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Input, Textarea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, } from '@asteby/metacore-ui/primitives';
|
|
21
22
|
import { Loader2 } from 'lucide-react';
|
|
22
23
|
import { ProcessStepper } from '@asteby/metacore-ui/wizard';
|
|
23
24
|
import { toast } from 'sonner';
|
|
24
|
-
import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
|
|
25
|
+
import { toastServerError, toastServerSuccess, extractFieldErrors, extractServerError, localizeFieldErrorMap } from './server-error';
|
|
26
|
+
import { FormErrorBanner } from './business/feedback';
|
|
27
|
+
import { CfdiStampResultDialog, extractStampResult } from './cfdi-stamp-panel';
|
|
25
28
|
import { useBranchCreateGate } from './branch-create-gate';
|
|
26
29
|
import { validateValues, bagHasErrors } from './validator';
|
|
27
30
|
import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
|
|
@@ -233,7 +236,14 @@ export function scalarDefaultFromRecord(field, record) {
|
|
|
233
236
|
}
|
|
234
237
|
return undefined;
|
|
235
238
|
}
|
|
236
|
-
export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
|
|
239
|
+
export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess: onSuccessProp, }) {
|
|
240
|
+
// Every successful action may have created/updated/deleted a record of
|
|
241
|
+
// `model` (federated component, wizard, generic form or confirmation), so
|
|
242
|
+
// announce it: mounted lists of the model reload on their own (#1020).
|
|
243
|
+
const onSuccess = useCallback(() => {
|
|
244
|
+
emitRecordMutation(model, 'update');
|
|
245
|
+
onSuccessProp?.();
|
|
246
|
+
}, [model, onSuccessProp]);
|
|
237
247
|
// Reactive read: a federated remote usually registers AFTER the first
|
|
238
248
|
// render, and a memoized lookup would pin the fallback forever.
|
|
239
249
|
const readComponent = () => getActionComponent(model, action.key);
|
|
@@ -518,10 +528,28 @@ function toastActionError(err, fields, t, language) {
|
|
|
518
528
|
}
|
|
519
529
|
toastServerError(err, { t, language });
|
|
520
530
|
}
|
|
531
|
+
/** Text for the in-dialog error banner of a failed action (PIT-046, #1023): a
|
|
532
|
+
* 422 with per-field `errors` -> "Revisa los campos" + each localized line; any
|
|
533
|
+
* other failure (500, network) -> the server's own message and cause. Unlike
|
|
534
|
+
* the toast it stays on screen until the next attempt. */
|
|
535
|
+
export function actionErrorBannerMessage(err, fields, t, language) {
|
|
536
|
+
const localized = localizeActionFieldErrors(err, fields, t, language);
|
|
537
|
+
if (localized) {
|
|
538
|
+
const head = t('validation.failed', { defaultValue: validationCatalog(language).failed });
|
|
539
|
+
const lines = Object.values(localized).filter(Boolean);
|
|
540
|
+
return lines.length ? `${head}: ${lines.join(' · ')}` : head;
|
|
541
|
+
}
|
|
542
|
+
const fallback = t('common.error', { defaultValue: 'Algo salió mal. Intenta de nuevo.' });
|
|
543
|
+
const ex = extractServerError(err, fallback);
|
|
544
|
+
const title = t(ex.title, { defaultValue: ex.title });
|
|
545
|
+
return ex.description && ex.description !== title ? `${title} — ${ex.description}` : title;
|
|
546
|
+
}
|
|
521
547
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
522
548
|
const { t, i18n } = useTranslation();
|
|
523
549
|
const api = useApi();
|
|
524
550
|
const [executing, setExecuting] = useState(false);
|
|
551
|
+
const [formError, setFormError] = useState();
|
|
552
|
+
const [stamp, setStamp] = useState();
|
|
525
553
|
// Actions listed in the model's `reason_required.actions` (cancel, void…)
|
|
526
554
|
// answer 422 errors.reason without a motive; the prompt asks and retries.
|
|
527
555
|
const reasonPrompt = useReasonPrompt();
|
|
@@ -532,6 +560,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
532
560
|
const label = t(action.label, { defaultValue: action.label });
|
|
533
561
|
const execute = async () => {
|
|
534
562
|
setExecuting(true);
|
|
563
|
+
setFormError(undefined);
|
|
535
564
|
try {
|
|
536
565
|
const url = buildActionUrl(endpoint, model, record.id, action.key);
|
|
537
566
|
// Supervisor PIN first (action.supervisorPolicy); null = cancelled.
|
|
@@ -548,21 +577,37 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
548
577
|
return;
|
|
549
578
|
if (res.data.success) {
|
|
550
579
|
toastServerSuccess(res.data, { t });
|
|
580
|
+
const result = extractStampResult(res.data);
|
|
581
|
+
if (result) {
|
|
582
|
+
setStamp(result);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
551
585
|
onOpenChange(false);
|
|
552
586
|
onSuccess();
|
|
553
587
|
}
|
|
554
588
|
else {
|
|
589
|
+
setFormError(actionErrorBannerMessage({ response: { data: res.data } }, action.fields, t, i18n.language));
|
|
555
590
|
toastActionError({ response: { data: res.data } }, action.fields, t, i18n.language);
|
|
556
591
|
}
|
|
557
592
|
}
|
|
558
593
|
catch (err) {
|
|
594
|
+
setFormError(actionErrorBannerMessage(err, action.fields, t, i18n.language));
|
|
559
595
|
toastActionError(err, action.fields, t, i18n.language);
|
|
560
596
|
}
|
|
561
597
|
finally {
|
|
562
598
|
setExecuting(false);
|
|
563
599
|
}
|
|
564
600
|
};
|
|
565
|
-
|
|
601
|
+
if (stamp) {
|
|
602
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: label, onOpenChange: (o) => {
|
|
603
|
+
if (o)
|
|
604
|
+
return;
|
|
605
|
+
setStamp(undefined);
|
|
606
|
+
onOpenChange(false);
|
|
607
|
+
onSuccess();
|
|
608
|
+
} }));
|
|
609
|
+
}
|
|
610
|
+
return (_jsxs(_Fragment, { children: [reasonPrompt.dialog, _jsx(AlertDialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsxs(AlertDialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), label] }), _jsx(AlertDialogDescription, { children: action.confirmMessage || `${label}?` })] }), record ? _jsx(RecordPreview, { model: model, record: record }) : null, _jsx(FormErrorBanner, { message: formError }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: executing, children: t('common.cancel') }), _jsxs(AlertDialogAction, { onClick: (e) => { e.preventDefault(); execute(); }, disabled: executing, style: action.color ? { backgroundColor: action.color } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), label] })] })] }) })] }));
|
|
566
611
|
}
|
|
567
612
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
568
613
|
const { t, i18n } = useTranslation();
|
|
@@ -578,6 +623,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
578
623
|
const [executing, setExecuting] = useState(false);
|
|
579
624
|
// Per-field validation errors (localized), shown inline under each input.
|
|
580
625
|
const [fieldErrors, setFieldErrors] = useState({});
|
|
626
|
+
// Persistent in-dialog banner for a failed submit (422 / 500) — the toast
|
|
627
|
+
// alone vanished and, for some failures, only the console had the cause.
|
|
628
|
+
const [formError, setFormError] = useState();
|
|
629
|
+
const [stamp, setStamp] = useState();
|
|
581
630
|
// Related records to surface BELOW the form, as read-only context for the
|
|
582
631
|
// record being acted on — e.g. the reception history of a transfer while
|
|
583
632
|
// receiving against it. Sourced from the model's metadata.relations (the
|
|
@@ -610,6 +659,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
610
659
|
if (open && action.fields) {
|
|
611
660
|
setFormData(buildFieldDefaults(action.fields, record));
|
|
612
661
|
setFieldErrors({});
|
|
662
|
+
setFormError(undefined);
|
|
613
663
|
}
|
|
614
664
|
}, [open, action.fields, record]);
|
|
615
665
|
// Lines the list row does not carry: load them from the declared relation
|
|
@@ -696,6 +746,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
696
746
|
};
|
|
697
747
|
const lang = i18n.language;
|
|
698
748
|
const handleActionError = (err) => {
|
|
749
|
+
setFormError(actionErrorBannerMessage(err, action.fields, t, lang));
|
|
699
750
|
const labels = labelsForValidationFields(action.fields, t);
|
|
700
751
|
const localized = localizeActionFieldErrors(err, action.fields, t, lang);
|
|
701
752
|
if (localized) {
|
|
@@ -733,6 +784,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
733
784
|
}
|
|
734
785
|
}
|
|
735
786
|
setFieldErrors({});
|
|
787
|
+
setFormError(undefined);
|
|
736
788
|
setExecuting(true);
|
|
737
789
|
try {
|
|
738
790
|
let payload = { ...formData };
|
|
@@ -765,6 +817,11 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
765
817
|
return;
|
|
766
818
|
if (res.data.success) {
|
|
767
819
|
toastServerSuccess(res.data, { t });
|
|
820
|
+
const result = extractStampResult(res.data);
|
|
821
|
+
if (result) {
|
|
822
|
+
setStamp(result);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
768
825
|
onOpenChange(false);
|
|
769
826
|
onSuccess();
|
|
770
827
|
}
|
|
@@ -794,17 +851,26 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
794
851
|
: hasLineItems
|
|
795
852
|
? '820px'
|
|
796
853
|
: undefined;
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
854
|
+
if (stamp) {
|
|
855
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
|
|
856
|
+
if (o)
|
|
857
|
+
return;
|
|
858
|
+
setStamp(undefined);
|
|
859
|
+
onOpenChange(false);
|
|
860
|
+
onSuccess();
|
|
861
|
+
} }));
|
|
862
|
+
}
|
|
863
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: tl(action.confirmMessage) })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
|
|
864
|
+
const fullWidth = isLineItemsField(field) ||
|
|
865
|
+
resolveWidget(field) === 'textarea' ||
|
|
866
|
+
resolveWidget(field) === 'richtext';
|
|
867
|
+
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
|
|
868
|
+
}), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] }), reasonPrompt.dialog] }) }));
|
|
803
869
|
}
|
|
804
870
|
// buildFieldDefaults seeds formData for a set of action fields, honoring the
|
|
805
871
|
// same line-items prefill spec + boolean/empty rules GenericActionModal uses, so
|
|
806
872
|
// wizard steps and single-page forms initialize identically.
|
|
807
|
-
function buildFieldDefaults(fields, record) {
|
|
873
|
+
export function buildFieldDefaults(fields, record) {
|
|
808
874
|
const defaults = {};
|
|
809
875
|
for (const field of fields) {
|
|
810
876
|
if (isLineItemsField(field)) {
|
|
@@ -851,6 +917,8 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
851
917
|
const [stepIndex, setStepIndex] = useState(0);
|
|
852
918
|
const [formData, setFormData] = useState({});
|
|
853
919
|
const [executing, setExecuting] = useState(false);
|
|
920
|
+
const [formError, setFormError] = useState();
|
|
921
|
+
const [stamp, setStamp] = useState();
|
|
854
922
|
// Reset to the first step and seed defaults for EVERY step's fields whenever
|
|
855
923
|
// the modal (re)opens, so accumulated values from a prior run don't leak.
|
|
856
924
|
useEffect(() => {
|
|
@@ -859,6 +927,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
859
927
|
const allFields = steps.flatMap((s) => s.fields ?? []);
|
|
860
928
|
setFormData(buildFieldDefaults(allFields, record));
|
|
861
929
|
setStepIndex(0);
|
|
930
|
+
setFormError(undefined);
|
|
862
931
|
}, [open, action.steps, record]);
|
|
863
932
|
const updateField = (key, value) => setFormData((prev) => ({ ...prev, [key]: value }));
|
|
864
933
|
const step = steps[stepIndex];
|
|
@@ -890,6 +959,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
890
959
|
}
|
|
891
960
|
}
|
|
892
961
|
setExecuting(true);
|
|
962
|
+
setFormError(undefined);
|
|
893
963
|
try {
|
|
894
964
|
const url = buildActionUrl(endpoint, model, record?.id, action.key);
|
|
895
965
|
const auth = await supervisor.authorize({ label: tl(action.label), supervisorPolicy: action.supervisorPolicy }, { model, recordId: record?.id != null ? String(record.id) : undefined });
|
|
@@ -902,16 +972,24 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
902
972
|
});
|
|
903
973
|
if (!res)
|
|
904
974
|
return;
|
|
975
|
+
const allFields = steps.flatMap(s => s.fields ?? []);
|
|
905
976
|
if (res.data.success) {
|
|
906
977
|
toastServerSuccess(res.data, { t });
|
|
978
|
+
const result = extractStampResult(res.data);
|
|
979
|
+
if (result) {
|
|
980
|
+
setStamp(result);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
907
983
|
onOpenChange(false);
|
|
908
984
|
onSuccess();
|
|
909
985
|
}
|
|
910
986
|
else {
|
|
911
|
-
|
|
987
|
+
setFormError(actionErrorBannerMessage({ response: { data: res.data } }, allFields, t, i18n.language));
|
|
988
|
+
toastActionError({ response: { data: res.data } }, allFields, t, i18n.language);
|
|
912
989
|
}
|
|
913
990
|
}
|
|
914
991
|
catch (err) {
|
|
992
|
+
setFormError(actionErrorBannerMessage(err, steps.flatMap(s => s.fields ?? []), t, i18n.language));
|
|
915
993
|
toastActionError(err, steps.flatMap(s => s.fields ?? []), t, i18n.language);
|
|
916
994
|
}
|
|
917
995
|
finally {
|
|
@@ -920,12 +998,21 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
920
998
|
};
|
|
921
999
|
if (steps.length === 0)
|
|
922
1000
|
return null;
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1001
|
+
if (stamp) {
|
|
1002
|
+
return (_jsx(CfdiStampResultDialog, { open: open, result: stamp, title: tl(action.label), onOpenChange: (o) => {
|
|
1003
|
+
if (o)
|
|
1004
|
+
return;
|
|
1005
|
+
setStamp(undefined);
|
|
1006
|
+
onOpenChange(false);
|
|
1007
|
+
onSuccess();
|
|
1008
|
+
} }));
|
|
1009
|
+
}
|
|
1010
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), _jsxs("div", { className: "pt-1", children: [_jsx(ProcessStepper, { steps: steps.map((s, i) => ({ key: String(i), label: s.title ? tl(s.title) : `${t('common.step', { defaultValue: 'Paso' })} ${i + 1}` })), activeIndex: stepIndex, onStepClick: i => setStepIndex(i) }), _jsxs(DialogDescription, { className: "pt-3", children: [t('common.step', { defaultValue: 'Paso' }), " ", stepIndex + 1, "/", steps.length, step?.title ? ` · ${tl(step.title)}` : ''] }), step?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: tl(step.description) }))] })] }), _jsxs("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: [_jsx(FormErrorBanner, { message: formError, className: "mb-3" }), _jsx(FieldGrid, { children: stepFields.map((field) => {
|
|
1011
|
+
const fullWidth = isLineItemsField(field) ||
|
|
1012
|
+
resolveWidget(field) === 'textarea' ||
|
|
1013
|
+
resolveWidget(field) === 'richtext';
|
|
1014
|
+
return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record)] }, field.key));
|
|
1015
|
+
}) })] }), _jsxs(DialogFooter, { className: "shrink-0", children: [stepIndex > 0 ? (_jsx(Button, { variant: "outline", onClick: goBack, disabled: executing, children: t('common.back', { defaultValue: 'Atrás' }) })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') })), isLast ? (_jsxs(Button, { onClick: submit, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })) : (_jsx(Button, { onClick: goNext, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: t('common.next', { defaultValue: 'Siguiente' }) }))] }), reasonPrompt.dialog] }) }));
|
|
929
1016
|
}
|
|
930
1017
|
function seedOptionFromRecord(field, value, record) {
|
|
931
1018
|
if (!record || !field.key.endsWith('_id'))
|
|
@@ -948,7 +1035,8 @@ function seedOptionFromRecord(field, value, record) {
|
|
|
948
1035
|
icon: sib.icon,
|
|
949
1036
|
};
|
|
950
1037
|
}
|
|
951
|
-
|
|
1038
|
+
/** Renders one action field (same widgets as the action modals). Shared with DocumentFormDialog. */
|
|
1039
|
+
export function renderField(field, value, onChange,
|
|
952
1040
|
// Full current form values — lets a line-items grid (and any cascading
|
|
953
1041
|
// header picker) resolve a `dependsOn` reference against sibling header
|
|
954
1042
|
// fields. Omitted by callers that have no surrounding form (the field is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/business/catalog.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,qBAAqB;IAClC,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,eAAO,MAAM,mBAAmB,EAAE,SAAS,qBAAqB,EAsGtD,CAAA"}
|
package/dist/business/catalog.js
CHANGED
|
@@ -76,4 +76,28 @@ export const BUSINESS_COMPONENTS = [
|
|
|
76
76
|
'recommendationsToLines',
|
|
77
77
|
],
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
name: 'VehiclePicker',
|
|
81
|
+
value: 'value: VehicleResult | null',
|
|
82
|
+
events: ['onChange'],
|
|
83
|
+
permissions: ['<model>.create (alta rápida; default Vehicle → vehicle.create)'],
|
|
84
|
+
usedBy: ['POS', 'Recepción de vehículo', 'OT', 'Cotización', 'Pedido'],
|
|
85
|
+
helpers: ['describeVehicle'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'RelateDocuments',
|
|
89
|
+
value: 'value: RelatedDocument[] (related_document_id + relation_type)',
|
|
90
|
+
events: ['onChange'],
|
|
91
|
+
permissions: [],
|
|
92
|
+
usedBy: ['Nota de crédito', 'Devolución', 'Sustitución de CFDI', 'REP'],
|
|
93
|
+
helpers: ['serializeRelatedDocuments'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'PrintSendDialog',
|
|
97
|
+
value: 'document: PrintableDocument (+ open)',
|
|
98
|
+
events: ['onSend', 'onPrint', 'onOpenChange'],
|
|
99
|
+
permissions: [],
|
|
100
|
+
usedBy: ['Cotización', 'Factura', 'Nota de crédito', 'Pedido', 'OC'],
|
|
101
|
+
helpers: [],
|
|
102
|
+
},
|
|
79
103
|
];
|
package/dist/business/index.d.ts
CHANGED
|
@@ -16,4 +16,7 @@ export * from './return';
|
|
|
16
16
|
export * from './return-wizard';
|
|
17
17
|
export * from './inspection';
|
|
18
18
|
export * from './inspection-checklist';
|
|
19
|
+
export * from './vehicle-picker';
|
|
20
|
+
export * from './relate-documents';
|
|
21
|
+
export * from './print-send-dialog';
|
|
19
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,mBAAmB,CAAA;AACjC,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA"}
|
package/dist/business/index.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type SendChannel = 'email' | 'whatsapp';
|
|
2
|
+
export interface PrintableDocument {
|
|
3
|
+
folio: string;
|
|
4
|
+
/** URL del PDF ya generado. Sin ella no hay «Imprimir». */
|
|
5
|
+
pdfUrl?: string;
|
|
6
|
+
/** Texto de cabecera («Factura», «Cotización»). */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Destinos sugeridos (datos del cliente). */
|
|
9
|
+
email?: string;
|
|
10
|
+
phone?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PrintSendDialogProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
document: PrintableDocument;
|
|
16
|
+
/** Canales de envío disponibles. Default: ambos. Vacío = solo imprimir. */
|
|
17
|
+
channels?: SendChannel[];
|
|
18
|
+
/**
|
|
19
|
+
* Envío a cargo del host. Si rechaza, el mensaje se muestra en el diálogo.
|
|
20
|
+
* Sin `onSend` no se ofrece enviar.
|
|
21
|
+
*/
|
|
22
|
+
onSend?: (channel: SendChannel, destination: string, document: PrintableDocument) => Promise<void> | void;
|
|
23
|
+
/** Reemplaza la impresión por defecto (abrir `pdfUrl` en otra pestaña). */
|
|
24
|
+
onPrint?: (document: PrintableDocument) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function PrintSendDialog({ open, onOpenChange, document: doc, channels, onSend, onPrint, }: PrintSendDialogProps): import("react").JSX.Element;
|
|
27
|
+
//# sourceMappingURL=print-send-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"print-send-dialog.d.ts","sourceRoot":"","sources":["../../src/business/print-send-dialog.tsx"],"names":[],"mappings":"AAmBA,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzG,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAA;CAClD;AAOD,wBAAgB,eAAe,CAAC,EAC5B,IAAI,EACJ,YAAY,EACZ,QAAQ,EAAE,GAAG,EACb,QAAgC,EAChC,MAAM,EACN,OAAO,GACV,EAAE,oBAAoB,+BAmHtB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// PrintSendDialog — imprimir o enviar un documento ya emitido (cotización,
|
|
3
|
+
// factura, nota de crédito…). El SDK NO llama a un proveedor de mensajería: el
|
|
4
|
+
// host recibe `onSend(channel, destination, document)` y decide qué hacer.
|
|
5
|
+
// Imprimir usa la URL del PDF que pasa el host (o su propio `onPrint`).
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Mail, MessageCircle, Printer, Loader2 } from 'lucide-react';
|
|
9
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, } from '@asteby/metacore-ui/primitives';
|
|
10
|
+
import { FormErrorBanner } from './feedback';
|
|
11
|
+
function defaultPrint(doc) {
|
|
12
|
+
if (!doc.pdfUrl || typeof window === 'undefined')
|
|
13
|
+
return;
|
|
14
|
+
window.open(doc.pdfUrl, '_blank', 'noopener,noreferrer');
|
|
15
|
+
}
|
|
16
|
+
export function PrintSendDialog({ open, onOpenChange, document: doc, channels = ['email', 'whatsapp'], onSend, onPrint, }) {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const [channel, setChannel] = useState(channels[0] ?? 'email');
|
|
19
|
+
const [destination, setDestination] = useState('');
|
|
20
|
+
const [sending, setSending] = useState(false);
|
|
21
|
+
const [sent, setSent] = useState(false);
|
|
22
|
+
const [error, setError] = useState();
|
|
23
|
+
const suggested = channel === 'email' ? doc.email : doc.phone;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (open) {
|
|
26
|
+
setDestination(suggested ?? '');
|
|
27
|
+
setSent(false);
|
|
28
|
+
setError(undefined);
|
|
29
|
+
}
|
|
30
|
+
}, [open, channel, suggested]);
|
|
31
|
+
const canSend = !!onSend && channels.length > 0;
|
|
32
|
+
const send = async () => {
|
|
33
|
+
const dest = destination.trim();
|
|
34
|
+
if (!onSend || !dest)
|
|
35
|
+
return;
|
|
36
|
+
setSending(true);
|
|
37
|
+
setError(undefined);
|
|
38
|
+
setSent(false);
|
|
39
|
+
try {
|
|
40
|
+
await onSend(channel, dest, doc);
|
|
41
|
+
setSent(true);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
const msg = e?.message;
|
|
45
|
+
setError(typeof msg === 'string' && msg
|
|
46
|
+
? msg
|
|
47
|
+
: t('printSend.error', { defaultValue: 'No se pudo enviar el documento. Inténtalo de nuevo.' }));
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setSending(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const channelLabel = (c) => c === 'email' ? t('printSend.email', { defaultValue: 'Correo' }) : t('printSend.whatsapp', { defaultValue: 'WhatsApp' });
|
|
54
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: [t('printSend.title', { defaultValue: 'Imprimir o enviar' }), doc.title ? ` · ${doc.title}` : ''] }), _jsx(DialogDescription, { children: doc.folio })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs(Button, { type: "button", variant: "outline", className: "w-full justify-start", disabled: !doc.pdfUrl && !onPrint, onClick: () => (onPrint ?? defaultPrint)(doc), children: [_jsx(Printer, { className: "mr-2 size-4", "aria-hidden": true }), t('printSend.print', { defaultValue: 'Imprimir' })] }), canSend && (_jsxs("div", { className: "space-y-2", children: [channels.length > 1 && (_jsx("div", { className: "flex gap-2", role: "group", "aria-label": t('printSend.channel', { defaultValue: 'Canal de envío' }), children: channels.map((c) => (_jsxs(Button, { type: "button", size: "sm", variant: c === channel ? 'default' : 'outline', "aria-pressed": c === channel, onClick: () => setChannel(c), children: [c === 'email' ? _jsx(Mail, { className: "mr-1.5 size-4", "aria-hidden": true }) : _jsx(MessageCircle, { className: "mr-1.5 size-4", "aria-hidden": true }), channelLabel(c)] }, c))) })), _jsx(Input, { value: destination, type: channel === 'email' ? 'email' : 'tel', "aria-label": t('printSend.destination', { defaultValue: 'Destino' }), placeholder: channel === 'email' ? 'cliente@ejemplo.com' : '+52 55 0000 0000', onChange: (e) => setDestination(e.target.value) }), _jsx(FormErrorBanner, { message: error }), sent && (_jsx("p", { role: "status", className: "text-sm text-emerald-600 dark:text-emerald-400", children: t('printSend.sent', { defaultValue: 'Enviado.' }) }))] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => onOpenChange(false), children: t('common.close', { defaultValue: 'Cerrar' }) }), canSend && (_jsxs(Button, { type: "button", onClick: send, disabled: sending || !destination.trim(), children: [sending && _jsx(Loader2, { className: "mr-2 size-4 animate-spin", "aria-hidden": true }), t('printSend.send', { defaultValue: 'Enviar' })] }))] })] }) }));
|
|
55
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface RelatableDocument {
|
|
2
|
+
id: string;
|
|
3
|
+
/** UUID fiscal (CFDI), cuando ya está timbrado. */
|
|
4
|
+
uuid?: string;
|
|
5
|
+
folio?: string;
|
|
6
|
+
/** Texto libre para mostrar («Factura A-120 · Cliente X»). */
|
|
7
|
+
label?: string;
|
|
8
|
+
total?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RelationTypeOption {
|
|
11
|
+
/** Clave que viaja en `relation_type` (p. ej. `"01"`). */
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
/** Un documento relacionado: lo que el formulario guarda. */
|
|
16
|
+
export interface RelatedDocument {
|
|
17
|
+
related_document_id: string;
|
|
18
|
+
relation_type: string;
|
|
19
|
+
/** Solo para mostrar. */
|
|
20
|
+
uuid?: string;
|
|
21
|
+
folio?: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RelateDocumentsProps {
|
|
25
|
+
value: RelatedDocument[];
|
|
26
|
+
onChange: (value: RelatedDocument[]) => void;
|
|
27
|
+
/** Catálogo de tipos de relación (lo aporta el addon). */
|
|
28
|
+
relationTypes: RelationTypeOption[];
|
|
29
|
+
/** Búsqueda de documentos origen. Estable (useCallback). */
|
|
30
|
+
search: (q: string, signal: AbortSignal) => Promise<RelatableDocument[]>;
|
|
31
|
+
/** Tipo preseleccionado al agregar. Default: el primero del catálogo. */
|
|
32
|
+
defaultRelationType?: string;
|
|
33
|
+
/** Máximo de documentos relacionados. Sin tope por defecto. */
|
|
34
|
+
max?: number;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function RelateDocuments({ value, onChange, relationTypes, search, defaultRelationType, max, disabled, placeholder, }: RelateDocumentsProps): import("react").JSX.Element;
|
|
39
|
+
/** Valor a enviar al servidor: solo los datos, sin los campos de visualización. */
|
|
40
|
+
export declare function serializeRelatedDocuments(value: RelatedDocument[]): Array<Pick<RelatedDocument, 'related_document_id' | 'relation_type'>>;
|
|
41
|
+
//# sourceMappingURL=relate-documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relate-documents.d.ts","sourceRoot":"","sources":["../../src/business/relate-documents.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,aAAa,EAAE,MAAM,CAAA;IACrB,yBAAyB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,CAAA;IAC5C,0DAA0D;IAC1D,aAAa,EAAE,kBAAkB,EAAE,CAAA;IACnC,4DAA4D;IAC5D,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAA;IACxE,yEAAyE;IACzE,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,EACN,mBAAmB,EACnB,GAAG,EACH,QAAQ,EACR,WAAW,GACd,EAAE,oBAAoB,+BA+GtB;AAED,mFAAmF;AACnF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,eAAe,CAAC,CAAC,CAEzI"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// RelateDocuments — relaciona uno o más documentos origen (CFDI) desde un
|
|
3
|
+
// formulario: nota de crédito, devolución, sustitución. El valor es DATO
|
|
4
|
+
// (`related_document_id`, `relation_type`, UUID/folio para mostrar), no un flujo
|
|
5
|
+
// de un solo módulo. El SDK no embebe catálogos fiscales: los tipos de relación
|
|
6
|
+
// (p. ej. SAT 01/03/04) llegan por la prop `relationTypes`.
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Link2, Search, X } from 'lucide-react';
|
|
10
|
+
import { Badge, Button, Input } from '@asteby/metacore-ui';
|
|
11
|
+
import { useAsyncSearch } from './use-async-search';
|
|
12
|
+
function docTitle(d) {
|
|
13
|
+
return d.label || d.folio || d.uuid || d.id || '';
|
|
14
|
+
}
|
|
15
|
+
export function RelateDocuments({ value, onChange, relationTypes, search, defaultRelationType, max, disabled, placeholder, }) {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const [text, setText] = useState('');
|
|
18
|
+
const [pendingType, setPendingType] = useState(defaultRelationType ?? relationTypes[0]?.value ?? '');
|
|
19
|
+
const { results, loading, error } = useAsyncSearch(text, search, { minChars: 2 });
|
|
20
|
+
const full = max != null && value.length >= max;
|
|
21
|
+
const typeLabel = (v) => relationTypes.find((r) => r.value === v)?.label ?? v;
|
|
22
|
+
const add = (d) => {
|
|
23
|
+
if (full || value.some((x) => x.related_document_id === d.id))
|
|
24
|
+
return;
|
|
25
|
+
onChange([
|
|
26
|
+
...value,
|
|
27
|
+
{ related_document_id: d.id, relation_type: pendingType, uuid: d.uuid, folio: d.folio, label: d.label },
|
|
28
|
+
]);
|
|
29
|
+
setText('');
|
|
30
|
+
};
|
|
31
|
+
const remove = (id) => onChange(value.filter((x) => x.related_document_id !== id));
|
|
32
|
+
const setType = (id, relation_type) => onChange(value.map((x) => (x.related_document_id === id ? { ...x, relation_type } : x)));
|
|
33
|
+
const selectCls = 'h-9 rounded-md border bg-background px-2 text-sm';
|
|
34
|
+
return (_jsxs("div", { "data-slot": "relate-documents", className: "space-y-2", children: [value.length > 0 && (_jsx("ul", { className: "divide-y rounded-md border", children: value.map((d) => (_jsxs("li", { className: "flex flex-wrap items-center gap-2 px-2 py-1.5", children: [_jsx(Link2, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: docTitle({ folio: d.folio, label: d.label, id: d.related_document_id }) }), d.uuid && _jsx("p", { className: "break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), _jsxs("select", { className: selectCls, disabled: disabled, value: d.relation_type, "aria-label": t('relateDocuments.type', { defaultValue: 'Tipo de relación' }), onChange: (e) => setType(d.related_document_id, e.target.value), children: [!relationTypes.some((r) => r.value === d.relation_type) && (_jsx("option", { value: d.relation_type, children: typeLabel(d.relation_type) })), relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value)))] }), !disabled && (_jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => remove(d.related_document_id), "aria-label": t('relateDocuments.remove', { defaultValue: 'Quitar relación' }), children: _jsx(X, { className: "size-4" }) }))] }, d.related_document_id))) })), !full && !disabled && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-muted-foreground", "aria-hidden": true }), _jsx(Input, { className: "pl-8", value: text, placeholder: placeholder ?? t('relateDocuments.placeholder', { defaultValue: 'Buscar por folio o UUID' }), onChange: (e) => setText(e.target.value) })] }), _jsx("select", { className: selectCls, value: pendingType, "aria-label": t('relateDocuments.newType', { defaultValue: 'Tipo para el documento nuevo' }), onChange: (e) => setPendingType(e.target.value), children: relationTypes.map((r) => (_jsx("option", { value: r.value, children: r.label }, r.value))) })] }), loading && _jsx("p", { className: "text-sm text-muted-foreground", children: t('common.searching', { defaultValue: 'Buscando…' }) }), error && !loading && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: t('relateDocuments.error', { defaultValue: 'No se pudo buscar documentos. Inténtalo de nuevo.' }) })), !loading && !error && text.trim().length >= 2 && results.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: t('relateDocuments.empty', { defaultValue: 'No encontramos ese documento.' }) })), results.length > 0 && (_jsx("ul", { role: "listbox", className: "max-h-64 divide-y overflow-auto rounded-md border", children: results.map((d) => {
|
|
35
|
+
const already = value.some((x) => x.related_document_id === d.id);
|
|
36
|
+
return (_jsx("li", { role: "option", "aria-selected": already, children: _jsxs("button", { type: "button", disabled: already, className: "flex w-full items-center justify-between gap-2 px-2 py-1.5 text-left hover:bg-accent disabled:opacity-50", onClick: () => add(d), children: [_jsxs("span", { children: [_jsx("span", { className: "block text-sm font-medium", children: docTitle(d) }), d.uuid && _jsx("span", { className: "block break-all font-mono text-xs text-muted-foreground", children: d.uuid })] }), already && _jsx(Badge, { variant: "muted", children: t('relateDocuments.added', { defaultValue: 'Agregado' }) })] }) }, d.id));
|
|
37
|
+
}) }))] }))] }));
|
|
38
|
+
}
|
|
39
|
+
/** Valor a enviar al servidor: solo los datos, sin los campos de visualización. */
|
|
40
|
+
export function serializeRelatedDocuments(value) {
|
|
41
|
+
return value.map(({ related_document_id, relation_type }) => ({ related_document_id, relation_type }));
|
|
42
|
+
}
|