@asteby/metacore-runtime-react 29.2.10 → 29.2.12
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 +12 -0
- package/dist/dialogs/export.d.ts.map +1 -1
- package/dist/dialogs/export.js +38 -10
- package/dist/dynamic-columns.d.ts +5 -4
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +21 -6
- package/package.json +1 -1
- package/src/__tests__/action-visibility-by-state.test.ts +20 -0
- package/src/dialogs/export.tsx +52 -13
- package/src/dynamic-columns.tsx +20 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 29.2.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0150e2f: ExportDialog localizes labels (core text + manifest i18n keys) and sends column_labels so CSV headers match the UI.
|
|
8
|
+
|
|
9
|
+
## 29.2.11
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9e07da0: Gate `requiresState` on row `status` or `state` so actions like purchases receive_goods hide on draft orders.
|
|
14
|
+
|
|
3
15
|
## 29.2.10
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/dialogs/export.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"export.d.ts","sourceRoot":"","sources":["../../src/dialogs/export.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAED,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,cAAc,EACd,gBAAgB,GACnB,EAAE,iBAAiB,+BAwUnB"}
|
package/dist/dialogs/export.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// ExportDialog — lets users pick format (csv/json) + columns and kicks off
|
|
3
3
|
// either a sync download or an async export job (polled via /exports/:id/status).
|
|
4
|
-
//
|
|
5
|
-
|
|
4
|
+
//
|
|
5
|
+
// Labels work for BOTH host styles:
|
|
6
|
+
// - Core DefineTable with human text ("Especialidades") → shown as-is via
|
|
7
|
+
// t(label, { defaultValue: label })
|
|
8
|
+
// - Manifest i18n keys ("purchases.field.state") → resolved by the host i18n
|
|
9
|
+
// bundle the same way DynamicTable already translates column headers
|
|
10
|
+
// The localized labels are also sent to the API as `column_labels` so the CSV
|
|
11
|
+
// headers match what the user sees in the dialog (backend may not have the
|
|
12
|
+
// addon locale bundles that only live on the frontend).
|
|
13
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
6
15
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Label, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
|
|
7
16
|
import { Progress, RadioGroup, RadioGroupItem } from './_primitives';
|
|
8
17
|
import { toast } from 'sonner';
|
|
@@ -10,6 +19,7 @@ import { Download, ChevronDown, Loader2 } from 'lucide-react';
|
|
|
10
19
|
import { useApi } from '../api-context';
|
|
11
20
|
export function ExportDialog({ open, onOpenChange, model, metadata, currentFilters, hasActiveFilters, }) {
|
|
12
21
|
const api = useApi();
|
|
22
|
+
const { t, i18n } = useTranslation();
|
|
13
23
|
const [format, setFormat] = useState('csv');
|
|
14
24
|
const [exportAll, setExportAll] = useState(false);
|
|
15
25
|
const [selectedColumns, setSelectedColumns] = useState([]);
|
|
@@ -17,11 +27,22 @@ export function ExportDialog({ open, onOpenChange, model, metadata, currentFilte
|
|
|
17
27
|
const [exporting, setExporting] = useState(false);
|
|
18
28
|
const [progress, setProgress] = useState(0);
|
|
19
29
|
const [asyncJobId, setAsyncJobId] = useState(null);
|
|
30
|
+
const tr = useCallback((label, fallback) => {
|
|
31
|
+
const raw = (label && label.trim()) || fallback || '';
|
|
32
|
+
if (!raw)
|
|
33
|
+
return fallback || '';
|
|
34
|
+
return t(raw, { defaultValue: raw });
|
|
35
|
+
}, [t]);
|
|
36
|
+
const title = useMemo(() => tr(metadata.titleKey || metadata.title, metadata.title || model), [metadata, model, tr]);
|
|
37
|
+
const visibleColumns = useMemo(() => (metadata?.columns?.filter((col) => !col.hidden) ?? []).map((col) => ({
|
|
38
|
+
...col,
|
|
39
|
+
displayLabel: tr(col.label, col.key),
|
|
40
|
+
})), [metadata, tr]);
|
|
20
41
|
useEffect(() => {
|
|
21
42
|
if (open && metadata?.columns) {
|
|
22
43
|
setSelectedColumns(metadata.columns
|
|
23
|
-
.filter(col => !col.hidden)
|
|
24
|
-
.map(col => col.key));
|
|
44
|
+
.filter((col) => !col.hidden)
|
|
45
|
+
.map((col) => col.key));
|
|
25
46
|
setFormat('csv');
|
|
26
47
|
setExportAll(false);
|
|
27
48
|
setColumnsOpen(false);
|
|
@@ -36,16 +57,14 @@ export function ExportDialog({ open, onOpenChange, model, metadata, currentFilte
|
|
|
36
57
|
: [...prev, key]);
|
|
37
58
|
}, []);
|
|
38
59
|
const toggleAllColumns = useCallback(() => {
|
|
39
|
-
const visibleKeys =
|
|
40
|
-
.filter(col => !col.hidden)
|
|
41
|
-
.map(col => col.key);
|
|
60
|
+
const visibleKeys = visibleColumns.map((col) => col.key);
|
|
42
61
|
if (selectedColumns.length === visibleKeys.length) {
|
|
43
62
|
setSelectedColumns([]);
|
|
44
63
|
}
|
|
45
64
|
else {
|
|
46
65
|
setSelectedColumns(visibleKeys);
|
|
47
66
|
}
|
|
48
|
-
}, [
|
|
67
|
+
}, [visibleColumns, selectedColumns]);
|
|
49
68
|
useEffect(() => {
|
|
50
69
|
if (!asyncJobId)
|
|
51
70
|
return;
|
|
@@ -99,9 +118,19 @@ export function ExportDialog({ open, onOpenChange, model, metadata, currentFilte
|
|
|
99
118
|
setExporting(true);
|
|
100
119
|
setProgress(0);
|
|
101
120
|
try {
|
|
121
|
+
const columnLabels = {};
|
|
122
|
+
for (const col of visibleColumns) {
|
|
123
|
+
if (selectedColumns.includes(col.key)) {
|
|
124
|
+
columnLabels[col.key] = col.displayLabel;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
const params = {
|
|
103
128
|
format,
|
|
104
129
|
columns: selectedColumns.join(','),
|
|
130
|
+
// Localized headers so CSV matches the dialog (core text OR
|
|
131
|
+
// manifest i18n keys resolved by the host frontend).
|
|
132
|
+
column_labels: JSON.stringify(columnLabels),
|
|
133
|
+
lang: i18n.language || 'es',
|
|
105
134
|
};
|
|
106
135
|
if (!exportAll && currentFilters) {
|
|
107
136
|
Object.entries(currentFilters).forEach(([key, value]) => {
|
|
@@ -141,6 +170,5 @@ export function ExportDialog({ open, onOpenChange, model, metadata, currentFilte
|
|
|
141
170
|
toast.error('Error al exportar los datos');
|
|
142
171
|
}
|
|
143
172
|
};
|
|
144
|
-
|
|
145
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsxs(DialogTitle, { children: ["Exportar ", metadata.title] }), _jsx(DialogDescription, { children: "Selecciona el formato y las columnas a exportar." })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6 space-y-6", children: exporting ? (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Exportando datos..." }), _jsx(Progress, { value: progress }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: progress > 0 ? `${Math.round(progress)}%` : 'Preparando...' })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-3", children: [_jsx(Label, { className: "text-sm font-medium", children: "Formato" }), _jsxs(RadioGroup, { value: format, onValueChange: (val) => setFormat(val), className: "flex gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(RadioGroupItem, { value: "csv", id: "format-csv" }), _jsx(Label, { htmlFor: "format-csv", className: "font-normal cursor-pointer", children: "CSV" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(RadioGroupItem, { value: "json", id: "format-json" }), _jsx(Label, { htmlFor: "format-json", className: "font-normal cursor-pointer", children: "JSON" })] })] })] }), hasActiveFilters && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "export-all", checked: exportAll, onCheckedChange: (checked) => setExportAll(checked === true) }), _jsx(Label, { htmlFor: "export-all", className: "font-normal cursor-pointer text-sm", children: "Exportar todos los registros (ignorar filtros)" })] })), _jsxs(Collapsible, { open: columnsOpen, onOpenChange: setColumnsOpen, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "w-full justify-between px-0 hover:bg-transparent", children: [_jsxs("span", { className: "text-sm font-medium", children: ["Columnas (", selectedColumns.length, "/", visibleColumns.length, ")"] }), _jsx(ChevronDown, { className: `h-4 w-4 transition-transform ${columnsOpen ? 'rotate-180' : ''}` })] }) }), _jsxs(CollapsibleContent, { className: "space-y-2 pt-2", children: [_jsxs("div", { className: "flex items-center gap-2 pb-2 border-b", children: [_jsx(Checkbox, { id: "select-all-columns", checked: selectedColumns.length === visibleColumns.length, onCheckedChange: toggleAllColumns }), _jsx(Label, { htmlFor: "select-all-columns", className: "font-normal cursor-pointer text-sm", children: "Seleccionar todas" })] }), _jsx("div", { className: "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto", children: visibleColumns.map(col => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `col-${col.key}`, checked: selectedColumns.includes(col.key), onCheckedChange: () => toggleColumn(col.key) }), _jsx(Label, { htmlFor: `col-${col.key}`, className: "font-normal cursor-pointer text-sm truncate", children: col.label })] }, col.key))) })] })] })] })) }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: exporting, children: "Cancelar" }), !exporting && (_jsxs(Button, { onClick: handleExport, disabled: selectedColumns.length === 0, children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), "Exportar"] })), exporting && (_jsxs(Button, { disabled: true, children: [_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), "Exportando..."] }))] })] }) }));
|
|
173
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-md max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsxs(DialogTitle, { children: ["Exportar ", title] }), _jsx(DialogDescription, { children: "Selecciona el formato y las columnas a exportar." })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6 space-y-6", children: exporting ? (_jsxs("div", { className: "space-y-4", children: [_jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Exportando datos..." }), _jsx(Progress, { value: progress }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: progress > 0 ? `${Math.round(progress)}%` : 'Preparando...' })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-3", children: [_jsx(Label, { className: "text-sm font-medium", children: "Formato" }), _jsxs(RadioGroup, { value: format, onValueChange: (val) => setFormat(val), className: "flex gap-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(RadioGroupItem, { value: "csv", id: "format-csv" }), _jsx(Label, { htmlFor: "format-csv", className: "font-normal cursor-pointer", children: "CSV" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(RadioGroupItem, { value: "json", id: "format-json" }), _jsx(Label, { htmlFor: "format-json", className: "font-normal cursor-pointer", children: "JSON" })] })] })] }), hasActiveFilters && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "export-all", checked: exportAll, onCheckedChange: (checked) => setExportAll(checked === true) }), _jsx(Label, { htmlFor: "export-all", className: "font-normal cursor-pointer text-sm", children: "Exportar todos los registros (ignorar filtros)" })] })), _jsxs(Collapsible, { open: columnsOpen, onOpenChange: setColumnsOpen, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", className: "w-full justify-between px-0 hover:bg-transparent", children: [_jsxs("span", { className: "text-sm font-medium", children: ["Columnas (", selectedColumns.length, "/", visibleColumns.length, ")"] }), _jsx(ChevronDown, { className: `h-4 w-4 transition-transform ${columnsOpen ? 'rotate-180' : ''}` })] }) }), _jsxs(CollapsibleContent, { className: "space-y-2 pt-2", children: [_jsxs("div", { className: "flex items-center gap-2 pb-2 border-b", children: [_jsx(Checkbox, { id: "select-all-columns", checked: selectedColumns.length === visibleColumns.length, onCheckedChange: toggleAllColumns }), _jsx(Label, { htmlFor: "select-all-columns", className: "font-normal cursor-pointer text-sm", children: "Seleccionar todas" })] }), _jsx("div", { className: "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto", children: visibleColumns.map(col => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `col-${col.key}`, checked: selectedColumns.includes(col.key), onCheckedChange: () => toggleColumn(col.key) }), _jsx(Label, { htmlFor: `col-${col.key}`, className: "font-normal cursor-pointer text-sm truncate", children: col.displayLabel })] }, col.key))) })] })] })] })) }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: exporting, children: "Cancelar" }), !exporting && (_jsxs(Button, { onClick: handleExport, disabled: selectedColumns.length === 0, children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), "Exportar"] })), exporting && (_jsxs(Button, { disabled: true, children: [_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" }), "Exportando..."] }))] })] }) }));
|
|
146
174
|
}
|
|
@@ -42,13 +42,14 @@ export declare const formatAggregateTotal: (col: ColumnDefinition, value: unknow
|
|
|
42
42
|
* State-machine gate for per-row actions.
|
|
43
43
|
*
|
|
44
44
|
* An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
|
|
45
|
-
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
46
|
-
* field
|
|
47
|
-
*
|
|
45
|
+
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
46
|
+
* lifecycle field (`status` or `state`) is contained in that array. This hides
|
|
47
|
+
* e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
|
|
48
|
+
* still in `draft`.
|
|
48
49
|
*
|
|
49
50
|
* Null-safe & non-regressive:
|
|
50
51
|
* - action without requiresState (or empty array) → always shown.
|
|
51
|
-
* - row with
|
|
52
|
+
* - row with neither `status` nor `state` → all actions shown.
|
|
52
53
|
*/
|
|
53
54
|
export declare const isActionAllowedForRowState: (action: any, row: any) => boolean;
|
|
54
55
|
/**
|
|
@@ -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;
|
|
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;AAsDD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAkC5D,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
|
@@ -106,24 +106,39 @@ const CodeCell = ({ text, maxLength }) => {
|
|
|
106
106
|
};
|
|
107
107
|
return (_jsxs("div", { className: "group flex items-center gap-1.5", children: [_jsx("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground/80", title: text, children: display }), _jsx("button", { type: "button", onClick: onCopy, className: "opacity-0 transition-opacity group-hover:opacity-100 text-muted-foreground hover:text-foreground", "aria-label": "Copiar", title: "Copiar", children: copied ? (_jsx(icons.Check, { className: "h-3.5 w-3.5 text-green-500" })) : (_jsx(icons.Copy, { className: "h-3.5 w-3.5" })) })] }));
|
|
108
108
|
};
|
|
109
|
+
/**
|
|
110
|
+
* Lifecycle column used by `requiresState`: prefer `status` (workshop, vehicles,
|
|
111
|
+
* …) and fall back to `state` (purchases, inventory transfers, …). Empty string
|
|
112
|
+
* is treated as missing so a blank `status` does not hide a populated `state`.
|
|
113
|
+
*/
|
|
114
|
+
const rowLifecycleState = (row) => {
|
|
115
|
+
const status = row?.status;
|
|
116
|
+
if (status !== undefined && status !== null && status !== '')
|
|
117
|
+
return status;
|
|
118
|
+
const state = row?.state;
|
|
119
|
+
if (state !== undefined && state !== null && state !== '')
|
|
120
|
+
return state;
|
|
121
|
+
return undefined;
|
|
122
|
+
};
|
|
109
123
|
/**
|
|
110
124
|
* State-machine gate for per-row actions.
|
|
111
125
|
*
|
|
112
126
|
* An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
|
|
113
|
-
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
114
|
-
* field
|
|
115
|
-
*
|
|
127
|
+
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
128
|
+
* lifecycle field (`status` or `state`) is contained in that array. This hides
|
|
129
|
+
* e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
|
|
130
|
+
* still in `draft`.
|
|
116
131
|
*
|
|
117
132
|
* Null-safe & non-regressive:
|
|
118
133
|
* - action without requiresState (or empty array) → always shown.
|
|
119
|
-
* - row with
|
|
134
|
+
* - row with neither `status` nor `state` → all actions shown.
|
|
120
135
|
*/
|
|
121
136
|
export const isActionAllowedForRowState = (action, row) => {
|
|
122
137
|
const requires = action?.requiresState ?? action?.requires_state;
|
|
123
138
|
if (!Array.isArray(requires) || requires.length === 0)
|
|
124
139
|
return true;
|
|
125
|
-
const status = row
|
|
126
|
-
if (status === undefined
|
|
140
|
+
const status = rowLifecycleState(row);
|
|
141
|
+
if (status === undefined)
|
|
127
142
|
return true;
|
|
128
143
|
return requires.map(String).includes(String(status));
|
|
129
144
|
};
|
package/package.json
CHANGED
|
@@ -48,4 +48,24 @@ describe('isActionAllowedForRowState', () => {
|
|
|
48
48
|
expect(isActionAllowedForRowState(action, { status: 2 })).toBe(true)
|
|
49
49
|
expect(isActionAllowedForRowState(action, { status: '3' })).toBe(false)
|
|
50
50
|
})
|
|
51
|
+
|
|
52
|
+
it('falls back to row.state when status is absent (purchases, transfers)', () => {
|
|
53
|
+
const receive = { key: 'receive_goods', requiresState: ['confirmed', 'partial'] }
|
|
54
|
+
expect(isActionAllowedForRowState(receive, { state: 'draft' })).toBe(false)
|
|
55
|
+
expect(isActionAllowedForRowState(receive, { state: 'confirmed' })).toBe(true)
|
|
56
|
+
expect(isActionAllowedForRowState(receive, { state: 'partial' })).toBe(true)
|
|
57
|
+
expect(isActionAllowedForRowState(receive, { state: 'received' })).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('prefers status over state when both are set', () => {
|
|
61
|
+
const action = { key: 'start', requiresState: ['reception'] }
|
|
62
|
+
expect(isActionAllowedForRowState(action, { status: 'reception', state: 'draft' })).toBe(true)
|
|
63
|
+
expect(isActionAllowedForRowState(action, { status: 'in_progress', state: 'reception' })).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('ignores empty status and uses state instead', () => {
|
|
67
|
+
const action = { key: 'confirm', requiresState: ['draft'] }
|
|
68
|
+
expect(isActionAllowedForRowState(action, { status: '', state: 'draft' })).toBe(true)
|
|
69
|
+
expect(isActionAllowedForRowState(action, { status: '', state: 'confirmed' })).toBe(false)
|
|
70
|
+
})
|
|
51
71
|
})
|
package/src/dialogs/export.tsx
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// ExportDialog — lets users pick format (csv/json) + columns and kicks off
|
|
2
2
|
// either a sync download or an async export job (polled via /exports/:id/status).
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
//
|
|
4
|
+
// Labels work for BOTH host styles:
|
|
5
|
+
// - Core DefineTable with human text ("Especialidades") → shown as-is via
|
|
6
|
+
// t(label, { defaultValue: label })
|
|
7
|
+
// - Manifest i18n keys ("purchases.field.state") → resolved by the host i18n
|
|
8
|
+
// bundle the same way DynamicTable already translates column headers
|
|
9
|
+
// The localized labels are also sent to the API as `column_labels` so the CSV
|
|
10
|
+
// headers match what the user sees in the dialog (backend may not have the
|
|
11
|
+
// addon locale bundles that only live on the frontend).
|
|
12
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
13
|
+
import { useTranslation } from 'react-i18next'
|
|
5
14
|
import {
|
|
6
15
|
Dialog,
|
|
7
16
|
DialogContent,
|
|
@@ -40,6 +49,7 @@ export function ExportDialog({
|
|
|
40
49
|
hasActiveFilters,
|
|
41
50
|
}: ExportDialogProps) {
|
|
42
51
|
const api = useApi()
|
|
52
|
+
const { t, i18n } = useTranslation()
|
|
43
53
|
const [format, setFormat] = useState<'csv' | 'json'>('csv')
|
|
44
54
|
const [exportAll, setExportAll] = useState(false)
|
|
45
55
|
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
|
|
@@ -48,12 +58,34 @@ export function ExportDialog({
|
|
|
48
58
|
const [progress, setProgress] = useState(0)
|
|
49
59
|
const [asyncJobId, setAsyncJobId] = useState<string | null>(null)
|
|
50
60
|
|
|
61
|
+
const tr = useCallback(
|
|
62
|
+
(label?: string, fallback?: string) => {
|
|
63
|
+
const raw = (label && label.trim()) || fallback || ''
|
|
64
|
+
if (!raw) return fallback || ''
|
|
65
|
+
return t(raw, { defaultValue: raw })
|
|
66
|
+
},
|
|
67
|
+
[t],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const title = useMemo(
|
|
71
|
+
() => tr((metadata as { titleKey?: string }).titleKey || metadata.title, metadata.title || model),
|
|
72
|
+
[metadata, model, tr],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const visibleColumns = useMemo(
|
|
76
|
+
() =>
|
|
77
|
+
(metadata?.columns?.filter((col) => !col.hidden) ?? []).map((col) => ({
|
|
78
|
+
...col,
|
|
79
|
+
displayLabel: tr(col.label, col.key),
|
|
80
|
+
})),
|
|
81
|
+
[metadata, tr],
|
|
82
|
+
)
|
|
51
83
|
useEffect(() => {
|
|
52
84
|
if (open && metadata?.columns) {
|
|
53
85
|
setSelectedColumns(
|
|
54
86
|
metadata.columns
|
|
55
|
-
.filter(col => !col.hidden)
|
|
56
|
-
.map(col => col.key)
|
|
87
|
+
.filter((col) => !col.hidden)
|
|
88
|
+
.map((col) => col.key),
|
|
57
89
|
)
|
|
58
90
|
setFormat('csv')
|
|
59
91
|
setExportAll(false)
|
|
@@ -68,21 +100,19 @@ export function ExportDialog({
|
|
|
68
100
|
setSelectedColumns((prev: string[]) =>
|
|
69
101
|
prev.includes(key)
|
|
70
102
|
? prev.filter((k: string) => k !== key)
|
|
71
|
-
: [...prev, key]
|
|
103
|
+
: [...prev, key],
|
|
72
104
|
)
|
|
73
105
|
}, [])
|
|
74
106
|
|
|
75
107
|
const toggleAllColumns = useCallback(() => {
|
|
76
|
-
const visibleKeys =
|
|
77
|
-
.filter(col => !col.hidden)
|
|
78
|
-
.map(col => col.key)
|
|
108
|
+
const visibleKeys = visibleColumns.map((col) => col.key)
|
|
79
109
|
|
|
80
110
|
if (selectedColumns.length === visibleKeys.length) {
|
|
81
111
|
setSelectedColumns([])
|
|
82
112
|
} else {
|
|
83
113
|
setSelectedColumns(visibleKeys)
|
|
84
114
|
}
|
|
85
|
-
}, [
|
|
115
|
+
}, [visibleColumns, selectedColumns])
|
|
86
116
|
|
|
87
117
|
useEffect(() => {
|
|
88
118
|
if (!asyncJobId) return
|
|
@@ -145,9 +175,20 @@ export function ExportDialog({
|
|
|
145
175
|
setProgress(0)
|
|
146
176
|
|
|
147
177
|
try {
|
|
178
|
+
const columnLabels: Record<string, string> = {}
|
|
179
|
+
for (const col of visibleColumns) {
|
|
180
|
+
if (selectedColumns.includes(col.key)) {
|
|
181
|
+
columnLabels[col.key] = col.displayLabel
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
148
185
|
const params: Record<string, any> = {
|
|
149
186
|
format,
|
|
150
187
|
columns: selectedColumns.join(','),
|
|
188
|
+
// Localized headers so CSV matches the dialog (core text OR
|
|
189
|
+
// manifest i18n keys resolved by the host frontend).
|
|
190
|
+
column_labels: JSON.stringify(columnLabels),
|
|
191
|
+
lang: i18n.language || 'es',
|
|
151
192
|
}
|
|
152
193
|
|
|
153
194
|
if (!exportAll && currentFilters) {
|
|
@@ -190,13 +231,11 @@ export function ExportDialog({
|
|
|
190
231
|
}
|
|
191
232
|
}
|
|
192
233
|
|
|
193
|
-
const visibleColumns = metadata?.columns?.filter(col => !col.hidden) ?? []
|
|
194
|
-
|
|
195
234
|
return (
|
|
196
235
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
197
236
|
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden">
|
|
198
237
|
<DialogHeader className="p-6 pb-4 border-b shrink-0">
|
|
199
|
-
<DialogTitle>Exportar {
|
|
238
|
+
<DialogTitle>Exportar {title}</DialogTitle>
|
|
200
239
|
<DialogDescription>
|
|
201
240
|
Selecciona el formato y las columnas a exportar.
|
|
202
241
|
</DialogDescription>
|
|
@@ -301,7 +340,7 @@ export function ExportDialog({
|
|
|
301
340
|
htmlFor={`col-${col.key}`}
|
|
302
341
|
className="font-normal cursor-pointer text-sm truncate"
|
|
303
342
|
>
|
|
304
|
-
{col.
|
|
343
|
+
{col.displayLabel}
|
|
305
344
|
</Label>
|
|
306
345
|
</div>
|
|
307
346
|
))}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -204,23 +204,37 @@ const CodeCell: React.FC<{ text: string; maxLength?: number }> = ({ text, maxLen
|
|
|
204
204
|
)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Lifecycle column used by `requiresState`: prefer `status` (workshop, vehicles,
|
|
209
|
+
* …) and fall back to `state` (purchases, inventory transfers, …). Empty string
|
|
210
|
+
* is treated as missing so a blank `status` does not hide a populated `state`.
|
|
211
|
+
*/
|
|
212
|
+
const rowLifecycleState = (row: any): unknown => {
|
|
213
|
+
const status = row?.status
|
|
214
|
+
if (status !== undefined && status !== null && status !== '') return status
|
|
215
|
+
const state = row?.state
|
|
216
|
+
if (state !== undefined && state !== null && state !== '') return state
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
207
220
|
/**
|
|
208
221
|
* State-machine gate for per-row actions.
|
|
209
222
|
*
|
|
210
223
|
* An action that declares a non-empty `requiresState` (camelCase) / `requires_state`
|
|
211
|
-
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
212
|
-
* field
|
|
213
|
-
*
|
|
224
|
+
* (snake_case, as served by some backends) is only surfaced for rows whose
|
|
225
|
+
* lifecycle field (`status` or `state`) is contained in that array. This hides
|
|
226
|
+
* e.g. "Recibir" (requiresState: ['confirmed','partial']) on a purchase order
|
|
227
|
+
* still in `draft`.
|
|
214
228
|
*
|
|
215
229
|
* Null-safe & non-regressive:
|
|
216
230
|
* - action without requiresState (or empty array) → always shown.
|
|
217
|
-
* - row with
|
|
231
|
+
* - row with neither `status` nor `state` → all actions shown.
|
|
218
232
|
*/
|
|
219
233
|
export const isActionAllowedForRowState = (action: any, row: any): boolean => {
|
|
220
234
|
const requires: unknown = action?.requiresState ?? action?.requires_state
|
|
221
235
|
if (!Array.isArray(requires) || requires.length === 0) return true
|
|
222
|
-
const status = row
|
|
223
|
-
if (status === undefined
|
|
236
|
+
const status = rowLifecycleState(row)
|
|
237
|
+
if (status === undefined) return true
|
|
224
238
|
return requires.map(String).includes(String(status))
|
|
225
239
|
}
|
|
226
240
|
|