@asteby/metacore-runtime-react 24.0.0 → 24.0.2
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 +30 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +141 -2
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +11 -4
- package/dist/dialogs/dynamic-record.d.ts +3 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +3 -3
- package/dist/display-value.d.ts +9 -0
- package/dist/display-value.d.ts.map +1 -1
- package/dist/display-value.js +1 -1
- package/dist/dynamic-columns.d.ts +9 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +23 -2
- package/package.json +5 -5
- package/src/action-modal-dispatcher.tsx +177 -2
- package/src/collection-cell.tsx +25 -4
- package/src/dialogs/dynamic-record.tsx +3 -3
- package/src/display-value.tsx +22 -2
- package/src/dynamic-columns.tsx +32 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 24.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a27d1a2: feat(relations): secondary identifier (SKU/email) under a reference chip
|
|
8
|
+
|
|
9
|
+
Reference chips (resolved FK cells, endpoint-option badges, jsonb line-item refs,
|
|
10
|
+
confirm-modal items) now render a muted secondary line under the label — a
|
|
11
|
+
product's SKU, a user's email — so a resolved record reads "Name / SKU" instead
|
|
12
|
+
of a bare name. Declarative: the SDK reads the backend-projected `subtitle` /
|
|
13
|
+
`description` on the option or resolved sibling (never a hardcoded column). Absent
|
|
14
|
+
→ single-line as before. The select picker already surfaced `description`; this
|
|
15
|
+
brings the resolved-chip surfaces in line.
|
|
16
|
+
|
|
17
|
+
## 24.0.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 4a62c3c: Preview automático del registro en el modal de confirmación de acciones de fila.
|
|
22
|
+
|
|
23
|
+
Al confirmar una row-action con `confirm` (p. ej. aceptar/rechazar un traspaso), el
|
|
24
|
+
`ConfirmActionDialog` ahora muestra un resumen compacto y de solo lectura del registro
|
|
25
|
+
que va a afectar, para no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la
|
|
26
|
+
metadata de tabla del modelo (labels + display hints, leída del cache o con un único fetch
|
|
27
|
+
a `/metadata/table/<model>`) y en los siblings de relación que la tabla ya resolvió sobre
|
|
28
|
+
la fila. La heurística surface relaciones resueltas a su label, campos line-items (jsonb,
|
|
29
|
+
como `Transfer.items`, renderizados producto × cantidad) y un puñado de escalares de
|
|
30
|
+
identidad; omite `id`, `organization_id`, timestamps y los `*_id` crudos sin label. Se
|
|
31
|
+
degrada solo: si no hay nada útil que mostrar, no renderiza la sección.
|
|
32
|
+
|
|
3
33
|
## 24.0.0
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
|
@@ -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":"AAwDA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCA+DlB"}
|
|
@@ -19,8 +19,10 @@ import { DynamicRelations } from './dynamic-relations';
|
|
|
19
19
|
import { DynamicSelectField } from './dynamic-select-field';
|
|
20
20
|
import { DynamicDateField } from './dynamic-date-field';
|
|
21
21
|
import { UploadField } from './upload-field';
|
|
22
|
-
import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema';
|
|
22
|
+
import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn, getFieldRef } from './dynamic-form-schema';
|
|
23
23
|
import { FieldGrid, FieldCell, FieldLabel } from './field-grid';
|
|
24
|
+
import { useMetadataCache } from './metadata-cache';
|
|
25
|
+
import { ViewValue, objectLabel, relationSiblingValue, isLineItemsField as isRecordLineItemsField, } from './dialogs/dynamic-record';
|
|
24
26
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
25
27
|
import { getActionComponent, } from '@asteby/metacore-sdk';
|
|
26
28
|
function isPrefillSpec(v) {
|
|
@@ -110,6 +112,143 @@ function buildActionUrl(endpoint, model, recordId, actionKey) {
|
|
|
110
112
|
}
|
|
111
113
|
return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`;
|
|
112
114
|
}
|
|
115
|
+
// ── Preview automático del registro en el modal de confirmación ─────────────
|
|
116
|
+
//
|
|
117
|
+
// Antes de confirmar una row-action (aceptar/rechazar un traspaso, etc.) el
|
|
118
|
+
// usuario ve un resumen compacto y read-only de QUÉ registro va a afectar, para
|
|
119
|
+
// no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la metadata de
|
|
120
|
+
// tabla del modelo (labels + display hints) y en los siblings de relación que la
|
|
121
|
+
// tabla ya resolvió sobre el `record`. Se degrada solo — si no hay ni un campo
|
|
122
|
+
// ni líneas útiles que mostrar, no renderiza nada (ni una caja vacía).
|
|
123
|
+
// Claves de sistema/infra que nunca aportan contexto al preview.
|
|
124
|
+
const PREVIEW_SKIP_KEYS = new Set([
|
|
125
|
+
'id',
|
|
126
|
+
'organization_id',
|
|
127
|
+
'org_id',
|
|
128
|
+
'created_at',
|
|
129
|
+
'updated_at',
|
|
130
|
+
'deleted_at',
|
|
131
|
+
'created_by',
|
|
132
|
+
'created_by_id',
|
|
133
|
+
'updated_by',
|
|
134
|
+
'updated_by_id',
|
|
135
|
+
]);
|
|
136
|
+
// Campos escalares "de identidad" que sí vale la pena anclar en el preview
|
|
137
|
+
// aunque no sean relación ni line-items (nombre, folio, estado, etc.).
|
|
138
|
+
const PREVIEW_INTEREST_KEYS = new Set([
|
|
139
|
+
'name',
|
|
140
|
+
'title',
|
|
141
|
+
'code',
|
|
142
|
+
'reference',
|
|
143
|
+
'folio',
|
|
144
|
+
'number',
|
|
145
|
+
'status',
|
|
146
|
+
'stage',
|
|
147
|
+
'state',
|
|
148
|
+
]);
|
|
149
|
+
const PREVIEW_INTEREST_STYLES = new Set(['status', 'badge', 'currency']);
|
|
150
|
+
const MAX_PREVIEW_FIELD_ROWS = 6;
|
|
151
|
+
function isEmptyPreviewValue(value) {
|
|
152
|
+
if (value === null || value === undefined || value === '')
|
|
153
|
+
return true;
|
|
154
|
+
if (Array.isArray(value))
|
|
155
|
+
return value.length === 0;
|
|
156
|
+
if (typeof value === 'object' && !(value instanceof Date))
|
|
157
|
+
return Object.keys(value).length === 0;
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
// selectPreviewColumns aplica la heurística compacta sobre las columnas del
|
|
161
|
+
// modelo: relaciones resueltas a su label, campos line-items (jsonb) y un puñado
|
|
162
|
+
// de escalares de identidad. Omite id/org/timestamps y los *_id crudos cuya
|
|
163
|
+
// relación no resolvió a un label legible.
|
|
164
|
+
function selectPreviewColumns(columns, record) {
|
|
165
|
+
if (!columns || !record)
|
|
166
|
+
return [];
|
|
167
|
+
const lineItemRows = [];
|
|
168
|
+
const fieldRows = [];
|
|
169
|
+
for (const col of columns) {
|
|
170
|
+
if (!col || !col.key)
|
|
171
|
+
continue;
|
|
172
|
+
if (col.hidden)
|
|
173
|
+
continue;
|
|
174
|
+
if (PREVIEW_SKIP_KEYS.has(col.key))
|
|
175
|
+
continue;
|
|
176
|
+
const value = record[col.key];
|
|
177
|
+
// Line-items (jsonb array como Transfer.items) → mini-tabla producto×cantidad.
|
|
178
|
+
if (isRecordLineItemsField(col, value)) {
|
|
179
|
+
if (isEmptyPreviewValue(value))
|
|
180
|
+
continue;
|
|
181
|
+
lineItemRows.push({ col, value, lineItems: true });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Relación (ref / search / dynamic_select / *_id): solo si el sibling
|
|
185
|
+
// resolvió a un label legible; si es un *_id crudo sin resolver, se omite.
|
|
186
|
+
const isRelation = !!getFieldRef(col) ||
|
|
187
|
+
col.type === 'search' ||
|
|
188
|
+
col.type === 'relation' ||
|
|
189
|
+
col.widget === 'dynamic_select' ||
|
|
190
|
+
(typeof col.key === 'string' && col.key.endsWith('_id'));
|
|
191
|
+
if (isRelation) {
|
|
192
|
+
const sib = relationSiblingValue(col, record);
|
|
193
|
+
const label = typeof sib === 'string' ? sib : objectLabel(sib);
|
|
194
|
+
if (!label)
|
|
195
|
+
continue;
|
|
196
|
+
fieldRows.push({ col, value, lineItems: false });
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Escalar de identidad (nombre/folio/estado/moneda…) con valor.
|
|
200
|
+
const styleKey = col.cellStyle ?? col.type;
|
|
201
|
+
const interesting = PREVIEW_INTEREST_KEYS.has(col.key) || PREVIEW_INTEREST_STYLES.has(String(styleKey));
|
|
202
|
+
if (interesting && !isEmptyPreviewValue(value)) {
|
|
203
|
+
fieldRows.push({ col, value, lineItems: false });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [...fieldRows.slice(0, MAX_PREVIEW_FIELD_ROWS), ...lineItemRows];
|
|
207
|
+
}
|
|
208
|
+
function RecordPreview({ model, record }) {
|
|
209
|
+
const { t } = useTranslation();
|
|
210
|
+
const api = useApi();
|
|
211
|
+
const cached = useMetadataCache((s) => s.getMetadata(model));
|
|
212
|
+
const setMetadata = useMetadataCache((s) => s.setMetadata);
|
|
213
|
+
const [fetched, setFetched] = useState(null);
|
|
214
|
+
// Sin metadata cacheada → UN fetch a /metadata/table/<model> (mismo patrón
|
|
215
|
+
// que model-action-toolbar). Se guarda en el store para próximos usos.
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
if (cached || !model)
|
|
218
|
+
return;
|
|
219
|
+
let cancelled = false;
|
|
220
|
+
api
|
|
221
|
+
.get(`/metadata/table/${model}`)
|
|
222
|
+
.then((res) => {
|
|
223
|
+
if (cancelled)
|
|
224
|
+
return;
|
|
225
|
+
const meta = (res.data?.data ?? res.data);
|
|
226
|
+
if (meta && Array.isArray(meta.columns)) {
|
|
227
|
+
setFetched(meta);
|
|
228
|
+
setMetadata(model, meta);
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.catch(() => {
|
|
232
|
+
if (!cancelled)
|
|
233
|
+
setFetched(null);
|
|
234
|
+
});
|
|
235
|
+
return () => {
|
|
236
|
+
cancelled = true;
|
|
237
|
+
};
|
|
238
|
+
}, [cached, model, api, setMetadata]);
|
|
239
|
+
const meta = cached ?? fetched;
|
|
240
|
+
const rows = useMemo(() => selectPreviewColumns(meta?.columns, record), [meta, record]);
|
|
241
|
+
// Degradación: nada útil que mostrar → no renderiza la sección.
|
|
242
|
+
if (rows.length === 0)
|
|
243
|
+
return null;
|
|
244
|
+
return (_jsx("div", { className: "mt-1 overflow-hidden rounded-md border bg-muted/30 text-sm", children: rows.map(({ col, value, lineItems }) => {
|
|
245
|
+
const label = t(col.label, { defaultValue: col.label });
|
|
246
|
+
if (lineItems) {
|
|
247
|
+
return (_jsxs("div", { className: "border-t px-3 py-2 first:border-t-0", children: [_jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: label }), _jsx(ViewValue, { field: col, value: value, record: record })] }, col.key));
|
|
248
|
+
}
|
|
249
|
+
return (_jsxs("div", { className: "flex items-baseline gap-3 border-t px-3 py-1.5 first:border-t-0", children: [_jsx("span", { className: "w-1/3 shrink-0 truncate text-muted-foreground", children: label }), _jsx("div", { className: "min-w-0 flex-1 [&_p]:py-0", children: _jsx(ViewValue, { field: col, value: value, record: record }) })] }, col.key));
|
|
250
|
+
}) }));
|
|
251
|
+
}
|
|
113
252
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
114
253
|
const { t } = useTranslation();
|
|
115
254
|
const api = useApi();
|
|
@@ -139,7 +278,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
139
278
|
setExecuting(false);
|
|
140
279
|
}
|
|
141
280
|
};
|
|
142
|
-
return (_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}?` })] }), _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] })] })] }) }));
|
|
281
|
+
return (_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, _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] })] })] }) }));
|
|
143
282
|
}
|
|
144
283
|
function GenericActionModal({ open, onOpenChange, action, model, record, endpoint, onSuccess }) {
|
|
145
284
|
const { t } = useTranslation();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkD9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;
|
|
1
|
+
{"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkD9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAwOD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAWR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAcR;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAkOD,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,CAAC,CAAC,EAAE,SAAS,CAAA;IACb;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,EACD,UAAU,EACV,WAAW,EACX,OAAiB,GACpB,EAAE,mBAAmB,qBA2HrB"}
|
package/dist/collection-cell.js
CHANGED
|
@@ -55,12 +55,16 @@ function resolvedRefFor(field, row) {
|
|
|
55
55
|
const obj = sibling;
|
|
56
56
|
const label = obj.label;
|
|
57
57
|
if (label !== undefined && label !== null && label !== '') {
|
|
58
|
+
const sub = obj.subtitle ?? obj.description;
|
|
58
59
|
return {
|
|
59
60
|
label: String(label),
|
|
60
61
|
value: obj.value,
|
|
61
62
|
image: typeof obj.image === 'string' && obj.image !== ''
|
|
62
63
|
? obj.image
|
|
63
64
|
: undefined,
|
|
65
|
+
subtitle: sub !== undefined && sub !== null && sub !== ''
|
|
66
|
+
? String(sub)
|
|
67
|
+
: undefined,
|
|
64
68
|
};
|
|
65
69
|
}
|
|
66
70
|
}
|
|
@@ -92,10 +96,12 @@ function resolvedRefObject(v) {
|
|
|
92
96
|
const label = v.label;
|
|
93
97
|
if (label === undefined || label === null || label === '')
|
|
94
98
|
return null;
|
|
99
|
+
const sub = v.subtitle ?? v.description;
|
|
95
100
|
return {
|
|
96
101
|
label: String(label),
|
|
97
102
|
value: v.value,
|
|
98
103
|
image: typeof v.image === 'string' && v.image !== '' ? v.image : undefined,
|
|
104
|
+
subtitle: sub !== undefined && sub !== null && sub !== '' ? String(sub) : undefined,
|
|
99
105
|
};
|
|
100
106
|
}
|
|
101
107
|
/**
|
|
@@ -104,9 +110,10 @@ function resolvedRefObject(v) {
|
|
|
104
110
|
* resolved name. The exact look the FK table columns use, so resolved relations
|
|
105
111
|
* read consistently everywhere (table popover, detail view, edit, line items).
|
|
106
112
|
*/
|
|
107
|
-
function RefChip({ label, image, getImageUrl, }) {
|
|
113
|
+
function RefChip({ label, image, subtitle, getImageUrl, }) {
|
|
108
114
|
const isDark = useIsDarkTheme();
|
|
109
|
-
|
|
115
|
+
const size = subtitle ? 24 : 18;
|
|
116
|
+
return (_jsxs("span", { className: "inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium", style: relationChipStyles(label, { isDark }), title: subtitle ? `${label} · ${subtitle}` : label, children: [image ? (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(image) : image, alt: label, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(label) })] })) : (_jsx(Box, { className: "h-3 w-3 shrink-0 opacity-70" })), subtitle ? (_jsxs("span", { className: "flex flex-col leading-tight min-w-0", children: [_jsx("span", { className: "truncate", children: label }), _jsx("span", { className: "truncate text-[0.65rem] opacity-70", children: subtitle })] })) : (_jsx("span", { className: "truncate", children: label }))] }));
|
|
110
117
|
}
|
|
111
118
|
/**
|
|
112
119
|
* Visual cell for one declared item-field. A resolved `ref` renders as a
|
|
@@ -117,7 +124,7 @@ function RefChip({ label, image, getImageUrl, }) {
|
|
|
117
124
|
function ItemFieldCell({ field, row, getImageUrl, }) {
|
|
118
125
|
const ref = resolvedRefFor(field, row);
|
|
119
126
|
if (ref?.label) {
|
|
120
|
-
return (_jsx(RefChip, { label: ref.label, image: ref.image, getImageUrl: getImageUrl }));
|
|
127
|
+
return (_jsx(RefChip, { label: ref.label, image: ref.image, subtitle: ref.subtitle, getImageUrl: getImageUrl }));
|
|
121
128
|
}
|
|
122
129
|
return _jsx(_Fragment, { children: formatScalar(row[field.key]) });
|
|
123
130
|
}
|
|
@@ -311,7 +318,7 @@ function MiniTable({ rows, locale, t, itemFields, getImageUrl, }) {
|
|
|
311
318
|
const ref = refKeys.has(key)
|
|
312
319
|
? resolvedRefObject(row[key])
|
|
313
320
|
: null;
|
|
314
|
-
return (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: ref ? (_jsx(RefChip, { label: ref.label, image: ref.image, getImageUrl: getImageUrl })) : (formatScalar(row[key])) }, key));
|
|
321
|
+
return (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: ref ? (_jsx(RefChip, { label: ref.label, image: ref.image, subtitle: ref.subtitle, getImageUrl: getImageUrl })) : (formatScalar(row[key])) }, key));
|
|
315
322
|
}) }, i))) })] }));
|
|
316
323
|
}
|
|
317
324
|
function ScalarList({ values }) {
|
|
@@ -159,6 +159,9 @@ export interface DynamicRecordDialogProps {
|
|
|
159
159
|
*/
|
|
160
160
|
onChange?: () => void;
|
|
161
161
|
}
|
|
162
|
+
export declare function objectLabel(value: any): string | undefined;
|
|
163
|
+
export declare function relationSiblingValue(field: FieldDef, record: any): any;
|
|
164
|
+
export declare function fieldItemFields(field: FieldDef): ItemField[] | undefined;
|
|
162
165
|
export declare function isLineItemsField(field: FieldDef, value: any): boolean;
|
|
163
166
|
export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
|
|
164
167
|
export declare function isMoneyField(field: FieldDef, value: any): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAuC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAejF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;CAC5B;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAuC1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAejF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;CAC5B;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CASrE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA6FD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAMD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,QAAQ,EAAE,CAMZ;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BA0Y1B;AAyDD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAWlF;AAsDD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BAgQA;AAsID,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE;IAC1D,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;CACf,+BA+KA"}
|
|
@@ -47,7 +47,7 @@ function resolvePath(obj, path) {
|
|
|
47
47
|
// objectLabel pulls a human label off a resolved relation/user object the
|
|
48
48
|
// backend serves: `{value,label}` (FK sibling), `{name,...}` (user object such
|
|
49
49
|
// as created_by), or `{title}`. Returns undefined for plain/empty objects.
|
|
50
|
-
function objectLabel(value) {
|
|
50
|
+
export function objectLabel(value) {
|
|
51
51
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
52
52
|
return undefined;
|
|
53
53
|
const label = value.label ?? value.name ?? value.title;
|
|
@@ -66,7 +66,7 @@ function pickImage(value) {
|
|
|
66
66
|
// FK column. A field `category_id` (search/dynamic_select/ref) ships a sibling
|
|
67
67
|
// `record.category = {value,label,image?}` (or a bare string/{name}); returns
|
|
68
68
|
// the raw sibling (object or string) so the caller can extract label + image.
|
|
69
|
-
function relationSiblingValue(field, record) {
|
|
69
|
+
export function relationSiblingValue(field, record) {
|
|
70
70
|
if (!record)
|
|
71
71
|
return undefined;
|
|
72
72
|
const candidates = [];
|
|
@@ -91,7 +91,7 @@ function relationSiblingValue(field, record) {
|
|
|
91
91
|
}
|
|
92
92
|
// fieldItemFields reads the declared jsonb line-items schema off a field,
|
|
93
93
|
// tolerating the snake_case `item_fields` alias the kernel serves.
|
|
94
|
-
function fieldItemFields(field) {
|
|
94
|
+
export function fieldItemFields(field) {
|
|
95
95
|
return field.itemFields ?? field.item_fields;
|
|
96
96
|
}
|
|
97
97
|
// isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
|
package/dist/display-value.d.ts
CHANGED
|
@@ -39,6 +39,15 @@ export interface DisplayOption {
|
|
|
39
39
|
icon?: string;
|
|
40
40
|
color?: string;
|
|
41
41
|
image?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Secondary identifier shown UNDER the label (muted, smaller) — a product's
|
|
44
|
+
* SKU/barcode, a user's email, etc. Lets a reference chip read like
|
|
45
|
+
* "Camiseta / SKU-001" instead of a bare name, so a resolved record is
|
|
46
|
+
* unambiguous. Populated declaratively: the backend projects it as the
|
|
47
|
+
* option/relation `description` (SearchConfig/FieldOptionsConfig.Description,
|
|
48
|
+
* or a column's `label_description`). Absent → single-line label as before.
|
|
49
|
+
*/
|
|
50
|
+
subtitle?: string;
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Colored option badge — the canonical "pro" pill for a select/status/badge
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAcA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAcA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CA8BA,CAAA"}
|
package/dist/display-value.js
CHANGED
|
@@ -70,5 +70,5 @@ export const OptionBadge = ({ option, getImageUrl }) => {
|
|
|
70
70
|
const isDark = useIsDarkTheme();
|
|
71
71
|
const colorSource = option.color || optionColor(option.value || option.label);
|
|
72
72
|
const colorStyles = generateBadgeStyles(colorSource, { isDark });
|
|
73
|
-
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), _jsx("span", { children: option.label })] }));
|
|
73
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.image ? (_jsx(RelationThumbnail, { src: option.image, alt: option.label, getImageUrl: getImageUrl, size: option.subtitle ? 22 : 16 })) : (option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" })), option.subtitle ? (_jsxs("span", { className: "flex flex-col leading-tight text-start", children: [_jsx("span", { children: option.label }), _jsx("span", { className: "text-[0.7rem] opacity-70", children: option.subtitle })] })) : (_jsx("span", { children: option.label }))] }));
|
|
74
74
|
};
|
|
@@ -112,6 +112,15 @@ export declare const resolveRelationLabel: (col: ColumnDefinition, row: any) =>
|
|
|
112
112
|
* the chip then renders text-only, exactly as before.
|
|
113
113
|
*/
|
|
114
114
|
export declare const resolveRelationImage: (col: ColumnDefinition, row: any) => string;
|
|
115
|
+
/**
|
|
116
|
+
* Reads a secondary identifier the backend stamps on a resolved FK sibling — a
|
|
117
|
+
* product's SKU, a user's email — projected as `subtitle`/`description` (the
|
|
118
|
+
* relational twin of `image` via the column's `label_description`). Rendered
|
|
119
|
+
* muted under the label so a reference chip reads "Name / SKU". Domain-agnostic:
|
|
120
|
+
* only the generic `subtitle`/`description` keys are read (never a hardcoded
|
|
121
|
+
* `sku`/`code`), so the author picks the column declaratively. '' when absent.
|
|
122
|
+
*/
|
|
123
|
+
export declare const resolveRelationSubtitle: (col: ColumnDefinition, row: any) => string;
|
|
115
124
|
/**
|
|
116
125
|
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
117
126
|
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAuC9C,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;
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAuC9C,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;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOzE,CAAA;AA8ID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAqmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -213,8 +213,11 @@ const BadgeWithEndpointOptions = ({ endpoint, value, getImageUrl }) => {
|
|
|
213
213
|
const { optionsMap } = React.useContext(OptionsContext);
|
|
214
214
|
const options = optionsMap.get(endpoint) || [];
|
|
215
215
|
const option = options.find((opt) => opt.value === value);
|
|
216
|
+
// Reference options carry the backend-projected `description` (the SKU/email
|
|
217
|
+
// the author pointed the options config at) — surface it as the chip subtitle
|
|
218
|
+
// so a resolved record reads "Name / SKU" instead of a bare name.
|
|
216
219
|
if (option)
|
|
217
|
-
return _jsx(OptionBadge, { option: option, fallback: String(value), getImageUrl: getImageUrl });
|
|
220
|
+
return _jsx(OptionBadge, { option: { ...option, subtitle: option.subtitle ?? option.description }, fallback: String(value), getImageUrl: getImageUrl });
|
|
218
221
|
// No declared option matched → humanize the raw token as a safety net so a
|
|
219
222
|
// cell never shows `in_progress` verbatim (option.label still wins above).
|
|
220
223
|
return _jsx(Badge, { variant: "outline", children: humanizeToken(value) });
|
|
@@ -336,6 +339,23 @@ export const resolveRelationImage = (col, row) => {
|
|
|
336
339
|
}
|
|
337
340
|
return '';
|
|
338
341
|
};
|
|
342
|
+
/**
|
|
343
|
+
* Reads a secondary identifier the backend stamps on a resolved FK sibling — a
|
|
344
|
+
* product's SKU, a user's email — projected as `subtitle`/`description` (the
|
|
345
|
+
* relational twin of `image` via the column's `label_description`). Rendered
|
|
346
|
+
* muted under the label so a reference chip reads "Name / SKU". Domain-agnostic:
|
|
347
|
+
* only the generic `subtitle`/`description` keys are read (never a hardcoded
|
|
348
|
+
* `sku`/`code`), so the author picks the column declaratively. '' when absent.
|
|
349
|
+
*/
|
|
350
|
+
export const resolveRelationSubtitle = (col, row) => {
|
|
351
|
+
const sibling = getNestedValue(row, relationKeyFor(col));
|
|
352
|
+
if (sibling && typeof sibling === 'object') {
|
|
353
|
+
const sub = sibling.subtitle ?? sibling.description;
|
|
354
|
+
if (sub !== undefined && sub !== null && sub !== '')
|
|
355
|
+
return String(sub);
|
|
356
|
+
}
|
|
357
|
+
return '';
|
|
358
|
+
};
|
|
339
359
|
/**
|
|
340
360
|
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
341
361
|
* backend-resolved sibling `{ value, label[, image] }` (see `relationKeyFor`)
|
|
@@ -350,12 +370,13 @@ const RelationCell = ({ col, row, getImageUrl }) => {
|
|
|
350
370
|
if (!display)
|
|
351
371
|
return _jsx(EmptyCell, {});
|
|
352
372
|
const image = resolveRelationImage(col, row);
|
|
373
|
+
const subtitle = resolveRelationSubtitle(col, row);
|
|
353
374
|
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
354
375
|
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
355
376
|
// alive yet stay visually distinct from option/status badges. Inline style
|
|
356
377
|
// (hex-derived) bypasses the host tailwind safelist.
|
|
357
378
|
const chipStyles = relationChipStyles(display, { isDark });
|
|
358
|
-
return (_jsxs("span", { className: "inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-medium", style: chipStyles, title: display, children: [image && (_jsx(RelationThumbnail, { src: image, alt: display, getImageUrl: getImageUrl, size: 18 })), _jsx("span", { className: "truncate", children: display })] }));
|
|
379
|
+
return (_jsxs("span", { className: "inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-medium", style: chipStyles, title: subtitle ? `${display} · ${subtitle}` : display, children: [image && (_jsx(RelationThumbnail, { src: image, alt: display, getImageUrl: getImageUrl, size: subtitle ? 24 : 18 })), subtitle ? (_jsxs("span", { className: "flex flex-col leading-tight min-w-0", children: [_jsx("span", { className: "truncate", children: display }), _jsx("span", { className: "truncate text-[0.7rem] opacity-70", children: subtitle })] })) : (_jsx("span", { className: "truncate", children: display }))] }));
|
|
359
380
|
};
|
|
360
381
|
/**
|
|
361
382
|
* Renders a SAP-style polymorphic source-document reference as a navigable
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "24.0.
|
|
3
|
+
"version": "24.0.2",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"react-i18next": ">=13",
|
|
38
38
|
"sonner": ">=1.7",
|
|
39
39
|
"zustand": ">=5",
|
|
40
|
-
"@asteby/metacore-sdk": "^3.2.
|
|
41
|
-
"@asteby/metacore-ui": "^2.10.
|
|
40
|
+
"@asteby/metacore-sdk": "^3.2.1",
|
|
41
|
+
"@asteby/metacore-ui": "^2.10.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
44
|
"@tanstack/react-router": {
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "^6.0.0",
|
|
68
68
|
"vitest": "^4.0.0",
|
|
69
69
|
"zustand": "^5.0.0",
|
|
70
|
-
"@asteby/metacore-sdk": "3.2.
|
|
71
|
-
"@asteby/metacore-ui": "2.10.
|
|
70
|
+
"@asteby/metacore-sdk": "3.2.1",
|
|
71
|
+
"@asteby/metacore-ui": "2.10.1"
|
|
72
72
|
},
|
|
73
73
|
"scripts": {
|
|
74
74
|
"build": "tsc -p tsconfig.json",
|
|
@@ -42,9 +42,17 @@ import { DynamicRelations } from './dynamic-relations'
|
|
|
42
42
|
import { DynamicSelectField } from './dynamic-select-field'
|
|
43
43
|
import { DynamicDateField } from './dynamic-date-field'
|
|
44
44
|
import { UploadField } from './upload-field'
|
|
45
|
-
import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema'
|
|
45
|
+
import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn, getFieldRef } from './dynamic-form-schema'
|
|
46
46
|
import { FieldGrid, FieldCell, FieldLabel } from './field-grid'
|
|
47
|
-
import
|
|
47
|
+
import { useMetadataCache } from './metadata-cache'
|
|
48
|
+
import {
|
|
49
|
+
ViewValue,
|
|
50
|
+
objectLabel,
|
|
51
|
+
relationSiblingValue,
|
|
52
|
+
fieldItemFields,
|
|
53
|
+
isLineItemsField as isRecordLineItemsField,
|
|
54
|
+
} from './dialogs/dynamic-record'
|
|
55
|
+
import type { ActionFieldDef, TableMetadata, ColumnDefinition } from './types'
|
|
48
56
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
49
57
|
import {
|
|
50
58
|
type ActionMetadata,
|
|
@@ -228,6 +236,172 @@ function buildActionUrl(endpoint: string | undefined, model: string, recordId: s
|
|
|
228
236
|
return hasRecord ? `/data/${model}/me/${recordId}/action/${actionKey}` : `/data/${model}/me/action/${actionKey}`
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
// ── Preview automático del registro en el modal de confirmación ─────────────
|
|
240
|
+
//
|
|
241
|
+
// Antes de confirmar una row-action (aceptar/rechazar un traspaso, etc.) el
|
|
242
|
+
// usuario ve un resumen compacto y read-only de QUÉ registro va a afectar, para
|
|
243
|
+
// no confirmar a ciegas. Es 100% del SDK y genérico: se apoya en la metadata de
|
|
244
|
+
// tabla del modelo (labels + display hints) y en los siblings de relación que la
|
|
245
|
+
// tabla ya resolvió sobre el `record`. Se degrada solo — si no hay ni un campo
|
|
246
|
+
// ni líneas útiles que mostrar, no renderiza nada (ni una caja vacía).
|
|
247
|
+
|
|
248
|
+
// Claves de sistema/infra que nunca aportan contexto al preview.
|
|
249
|
+
const PREVIEW_SKIP_KEYS = new Set([
|
|
250
|
+
'id',
|
|
251
|
+
'organization_id',
|
|
252
|
+
'org_id',
|
|
253
|
+
'created_at',
|
|
254
|
+
'updated_at',
|
|
255
|
+
'deleted_at',
|
|
256
|
+
'created_by',
|
|
257
|
+
'created_by_id',
|
|
258
|
+
'updated_by',
|
|
259
|
+
'updated_by_id',
|
|
260
|
+
])
|
|
261
|
+
|
|
262
|
+
// Campos escalares "de identidad" que sí vale la pena anclar en el preview
|
|
263
|
+
// aunque no sean relación ni line-items (nombre, folio, estado, etc.).
|
|
264
|
+
const PREVIEW_INTEREST_KEYS = new Set([
|
|
265
|
+
'name',
|
|
266
|
+
'title',
|
|
267
|
+
'code',
|
|
268
|
+
'reference',
|
|
269
|
+
'folio',
|
|
270
|
+
'number',
|
|
271
|
+
'status',
|
|
272
|
+
'stage',
|
|
273
|
+
'state',
|
|
274
|
+
])
|
|
275
|
+
const PREVIEW_INTEREST_STYLES = new Set(['status', 'badge', 'currency'])
|
|
276
|
+
|
|
277
|
+
const MAX_PREVIEW_FIELD_ROWS = 6
|
|
278
|
+
|
|
279
|
+
function isEmptyPreviewValue(value: any): boolean {
|
|
280
|
+
if (value === null || value === undefined || value === '') return true
|
|
281
|
+
if (Array.isArray(value)) return value.length === 0
|
|
282
|
+
if (typeof value === 'object' && !(value instanceof Date)) return Object.keys(value).length === 0
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
interface PreviewRow {
|
|
287
|
+
col: ColumnDefinition
|
|
288
|
+
value: any
|
|
289
|
+
lineItems: boolean
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// selectPreviewColumns aplica la heurística compacta sobre las columnas del
|
|
293
|
+
// modelo: relaciones resueltas a su label, campos line-items (jsonb) y un puñado
|
|
294
|
+
// de escalares de identidad. Omite id/org/timestamps y los *_id crudos cuya
|
|
295
|
+
// relación no resolvió a un label legible.
|
|
296
|
+
function selectPreviewColumns(columns: ColumnDefinition[] | undefined, record: any): PreviewRow[] {
|
|
297
|
+
if (!columns || !record) return []
|
|
298
|
+
const lineItemRows: PreviewRow[] = []
|
|
299
|
+
const fieldRows: PreviewRow[] = []
|
|
300
|
+
|
|
301
|
+
for (const col of columns) {
|
|
302
|
+
if (!col || !col.key) continue
|
|
303
|
+
if (col.hidden) continue
|
|
304
|
+
if (PREVIEW_SKIP_KEYS.has(col.key)) continue
|
|
305
|
+
const value = record[col.key]
|
|
306
|
+
|
|
307
|
+
// Line-items (jsonb array como Transfer.items) → mini-tabla producto×cantidad.
|
|
308
|
+
if (isRecordLineItemsField(col as any, value)) {
|
|
309
|
+
if (isEmptyPreviewValue(value)) continue
|
|
310
|
+
lineItemRows.push({ col, value, lineItems: true })
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Relación (ref / search / dynamic_select / *_id): solo si el sibling
|
|
315
|
+
// resolvió a un label legible; si es un *_id crudo sin resolver, se omite.
|
|
316
|
+
const isRelation =
|
|
317
|
+
!!getFieldRef(col as ActionFieldDef) ||
|
|
318
|
+
col.type === 'search' ||
|
|
319
|
+
col.type === 'relation' ||
|
|
320
|
+
(col as { widget?: string }).widget === 'dynamic_select' ||
|
|
321
|
+
(typeof col.key === 'string' && col.key.endsWith('_id'))
|
|
322
|
+
if (isRelation) {
|
|
323
|
+
const sib = relationSiblingValue(col as any, record)
|
|
324
|
+
const label = typeof sib === 'string' ? sib : objectLabel(sib)
|
|
325
|
+
if (!label) continue
|
|
326
|
+
fieldRows.push({ col, value, lineItems: false })
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Escalar de identidad (nombre/folio/estado/moneda…) con valor.
|
|
331
|
+
const styleKey = col.cellStyle ?? col.type
|
|
332
|
+
const interesting =
|
|
333
|
+
PREVIEW_INTEREST_KEYS.has(col.key) || PREVIEW_INTEREST_STYLES.has(String(styleKey))
|
|
334
|
+
if (interesting && !isEmptyPreviewValue(value)) {
|
|
335
|
+
fieldRows.push({ col, value, lineItems: false })
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return [...fieldRows.slice(0, MAX_PREVIEW_FIELD_ROWS), ...lineItemRows]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function RecordPreview({ model, record }: { model: string; record: any }) {
|
|
343
|
+
const { t } = useTranslation()
|
|
344
|
+
const api = useApi()
|
|
345
|
+
const cached = useMetadataCache((s) => s.getMetadata(model))
|
|
346
|
+
const setMetadata = useMetadataCache((s) => s.setMetadata)
|
|
347
|
+
const [fetched, setFetched] = useState<TableMetadata | null>(null)
|
|
348
|
+
|
|
349
|
+
// Sin metadata cacheada → UN fetch a /metadata/table/<model> (mismo patrón
|
|
350
|
+
// que model-action-toolbar). Se guarda en el store para próximos usos.
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (cached || !model) return
|
|
353
|
+
let cancelled = false
|
|
354
|
+
api
|
|
355
|
+
.get(`/metadata/table/${model}`)
|
|
356
|
+
.then((res) => {
|
|
357
|
+
if (cancelled) return
|
|
358
|
+
const meta = (res.data?.data ?? res.data) as TableMetadata
|
|
359
|
+
if (meta && Array.isArray(meta.columns)) {
|
|
360
|
+
setFetched(meta)
|
|
361
|
+
setMetadata(model, meta)
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
.catch(() => {
|
|
365
|
+
if (!cancelled) setFetched(null)
|
|
366
|
+
})
|
|
367
|
+
return () => {
|
|
368
|
+
cancelled = true
|
|
369
|
+
}
|
|
370
|
+
}, [cached, model, api, setMetadata])
|
|
371
|
+
|
|
372
|
+
const meta = cached ?? fetched
|
|
373
|
+
const rows = useMemo(() => selectPreviewColumns(meta?.columns, record), [meta, record])
|
|
374
|
+
|
|
375
|
+
// Degradación: nada útil que mostrar → no renderiza la sección.
|
|
376
|
+
if (rows.length === 0) return null
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div className="mt-1 overflow-hidden rounded-md border bg-muted/30 text-sm">
|
|
380
|
+
{rows.map(({ col, value, lineItems }) => {
|
|
381
|
+
const label = t(col.label, { defaultValue: col.label })
|
|
382
|
+
if (lineItems) {
|
|
383
|
+
return (
|
|
384
|
+
<div key={col.key} className="border-t px-3 py-2 first:border-t-0">
|
|
385
|
+
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
386
|
+
{label}
|
|
387
|
+
</div>
|
|
388
|
+
<ViewValue field={col as any} value={value} record={record} />
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
return (
|
|
393
|
+
<div key={col.key} className="flex items-baseline gap-3 border-t px-3 py-1.5 first:border-t-0">
|
|
394
|
+
<span className="w-1/3 shrink-0 truncate text-muted-foreground">{label}</span>
|
|
395
|
+
<div className="min-w-0 flex-1 [&_p]:py-0">
|
|
396
|
+
<ViewValue field={col as any} value={value} record={record} />
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
)
|
|
400
|
+
})}
|
|
401
|
+
</div>
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
231
405
|
function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoint, onSuccess }: ActionModalProps) {
|
|
232
406
|
const { t } = useTranslation()
|
|
233
407
|
const api = useApi()
|
|
@@ -268,6 +442,7 @@ function ConfirmActionDialog({ open, onOpenChange, action, model, record, endpoi
|
|
|
268
442
|
{action.confirmMessage || `${label}?`}
|
|
269
443
|
</AlertDialogDescription>
|
|
270
444
|
</AlertDialogHeader>
|
|
445
|
+
{record ? <RecordPreview model={model} record={record} /> : null}
|
|
271
446
|
<AlertDialogFooter>
|
|
272
447
|
<AlertDialogCancel disabled={executing}>{t('common.cancel')}</AlertDialogCancel>
|
|
273
448
|
<AlertDialogAction
|
package/src/collection-cell.tsx
CHANGED
|
@@ -92,6 +92,8 @@ interface ResolvedRef {
|
|
|
92
92
|
value?: unknown
|
|
93
93
|
/** Optional thumbnail (product photo, logo, avatar) resolved by the backend. */
|
|
94
94
|
image?: string
|
|
95
|
+
/** Optional secondary identifier (SKU/email) shown muted under the label. */
|
|
96
|
+
subtitle?: string
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/**
|
|
@@ -110,6 +112,7 @@ function resolvedRefFor(
|
|
|
110
112
|
const obj = sibling as Record<string, unknown>
|
|
111
113
|
const label = obj.label
|
|
112
114
|
if (label !== undefined && label !== null && label !== '') {
|
|
115
|
+
const sub = obj.subtitle ?? obj.description
|
|
113
116
|
return {
|
|
114
117
|
label: String(label),
|
|
115
118
|
value: obj.value,
|
|
@@ -117,6 +120,10 @@ function resolvedRefFor(
|
|
|
117
120
|
typeof obj.image === 'string' && obj.image !== ''
|
|
118
121
|
? obj.image
|
|
119
122
|
: undefined,
|
|
123
|
+
subtitle:
|
|
124
|
+
sub !== undefined && sub !== null && sub !== ''
|
|
125
|
+
? String(sub)
|
|
126
|
+
: undefined,
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
} else if (typeof sibling === 'string' && sibling !== '') {
|
|
@@ -148,11 +155,14 @@ function resolvedRefObject(
|
|
|
148
155
|
if (!isPlainObject(v)) return null
|
|
149
156
|
const label = v.label
|
|
150
157
|
if (label === undefined || label === null || label === '') return null
|
|
158
|
+
const sub = v.subtitle ?? v.description
|
|
151
159
|
return {
|
|
152
160
|
label: String(label),
|
|
153
161
|
value: v.value,
|
|
154
162
|
image:
|
|
155
163
|
typeof v.image === 'string' && v.image !== '' ? v.image : undefined,
|
|
164
|
+
subtitle:
|
|
165
|
+
sub !== undefined && sub !== null && sub !== '' ? String(sub) : undefined,
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
@@ -165,23 +175,26 @@ function resolvedRefObject(
|
|
|
165
175
|
function RefChip({
|
|
166
176
|
label,
|
|
167
177
|
image,
|
|
178
|
+
subtitle,
|
|
168
179
|
getImageUrl,
|
|
169
180
|
}: {
|
|
170
181
|
label: string
|
|
171
182
|
image?: string
|
|
183
|
+
subtitle?: string
|
|
172
184
|
getImageUrl?: (path: string) => string
|
|
173
185
|
}): React.ReactElement {
|
|
174
186
|
const isDark = useIsDarkTheme()
|
|
187
|
+
const size = subtitle ? 24 : 18
|
|
175
188
|
return (
|
|
176
189
|
<span
|
|
177
190
|
className="inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
|
|
178
191
|
style={relationChipStyles(label, { isDark })}
|
|
179
|
-
title={label}
|
|
192
|
+
title={subtitle ? `${label} · ${subtitle}` : label}
|
|
180
193
|
>
|
|
181
194
|
{image ? (
|
|
182
195
|
<Avatar
|
|
183
196
|
className="shrink-0 rounded-sm ring-1 ring-border/40"
|
|
184
|
-
style={{ width:
|
|
197
|
+
style={{ width: size, height: size }}
|
|
185
198
|
>
|
|
186
199
|
<AvatarImage
|
|
187
200
|
src={getImageUrl ? getImageUrl(image) : image}
|
|
@@ -195,7 +208,14 @@ function RefChip({
|
|
|
195
208
|
) : (
|
|
196
209
|
<Box className="h-3 w-3 shrink-0 opacity-70" />
|
|
197
210
|
)}
|
|
198
|
-
|
|
211
|
+
{subtitle ? (
|
|
212
|
+
<span className="flex flex-col leading-tight min-w-0">
|
|
213
|
+
<span className="truncate">{label}</span>
|
|
214
|
+
<span className="truncate text-[0.65rem] opacity-70">{subtitle}</span>
|
|
215
|
+
</span>
|
|
216
|
+
) : (
|
|
217
|
+
<span className="truncate">{label}</span>
|
|
218
|
+
)}
|
|
199
219
|
</span>
|
|
200
220
|
)
|
|
201
221
|
}
|
|
@@ -218,7 +238,7 @@ function ItemFieldCell({
|
|
|
218
238
|
const ref = resolvedRefFor(field, row)
|
|
219
239
|
if (ref?.label) {
|
|
220
240
|
return (
|
|
221
|
-
<RefChip label={ref.label} image={ref.image} getImageUrl={getImageUrl} />
|
|
241
|
+
<RefChip label={ref.label} image={ref.image} subtitle={ref.subtitle} getImageUrl={getImageUrl} />
|
|
222
242
|
)
|
|
223
243
|
}
|
|
224
244
|
return <>{formatScalar(row[field.key])}</>
|
|
@@ -495,6 +515,7 @@ function MiniTable({
|
|
|
495
515
|
<RefChip
|
|
496
516
|
label={ref.label}
|
|
497
517
|
image={ref.image}
|
|
518
|
+
subtitle={ref.subtitle}
|
|
498
519
|
getImageUrl={getImageUrl}
|
|
499
520
|
/>
|
|
500
521
|
) : (
|
|
@@ -267,7 +267,7 @@ function resolvePath(obj: any, path: string): any {
|
|
|
267
267
|
// objectLabel pulls a human label off a resolved relation/user object the
|
|
268
268
|
// backend serves: `{value,label}` (FK sibling), `{name,...}` (user object such
|
|
269
269
|
// as created_by), or `{title}`. Returns undefined for plain/empty objects.
|
|
270
|
-
function objectLabel(value: any): string | undefined {
|
|
270
|
+
export function objectLabel(value: any): string | undefined {
|
|
271
271
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
272
272
|
const label = value.label ?? value.name ?? value.title
|
|
273
273
|
if (label != null && label !== '') return String(label)
|
|
@@ -285,7 +285,7 @@ function pickImage(value: any): string | undefined {
|
|
|
285
285
|
// FK column. A field `category_id` (search/dynamic_select/ref) ships a sibling
|
|
286
286
|
// `record.category = {value,label,image?}` (or a bare string/{name}); returns
|
|
287
287
|
// the raw sibling (object or string) so the caller can extract label + image.
|
|
288
|
-
function relationSiblingValue(field: FieldDef, record: any): any {
|
|
288
|
+
export function relationSiblingValue(field: FieldDef, record: any): any {
|
|
289
289
|
if (!record) return undefined
|
|
290
290
|
const candidates: string[] = []
|
|
291
291
|
const ref = getFieldRef(field as ActionFieldDef)
|
|
@@ -305,7 +305,7 @@ function relationSiblingValue(field: FieldDef, record: any): any {
|
|
|
305
305
|
|
|
306
306
|
// fieldItemFields reads the declared jsonb line-items schema off a field,
|
|
307
307
|
// tolerating the snake_case `item_fields` alias the kernel serves.
|
|
308
|
-
function fieldItemFields(field: FieldDef): ItemField[] | undefined {
|
|
308
|
+
export function fieldItemFields(field: FieldDef): ItemField[] | undefined {
|
|
309
309
|
return field.itemFields ?? field.item_fields
|
|
310
310
|
}
|
|
311
311
|
|
package/src/display-value.tsx
CHANGED
|
@@ -98,6 +98,15 @@ export interface DisplayOption {
|
|
|
98
98
|
icon?: string
|
|
99
99
|
color?: string
|
|
100
100
|
image?: string
|
|
101
|
+
/**
|
|
102
|
+
* Secondary identifier shown UNDER the label (muted, smaller) — a product's
|
|
103
|
+
* SKU/barcode, a user's email, etc. Lets a reference chip read like
|
|
104
|
+
* "Camiseta / SKU-001" instead of a bare name, so a resolved record is
|
|
105
|
+
* unambiguous. Populated declaratively: the backend projects it as the
|
|
106
|
+
* option/relation `description` (SearchConfig/FieldOptionsConfig.Description,
|
|
107
|
+
* or a column's `label_description`). Absent → single-line label as before.
|
|
108
|
+
*/
|
|
109
|
+
subtitle?: string
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
/**
|
|
@@ -123,12 +132,23 @@ export const OptionBadge: React.FC<{
|
|
|
123
132
|
src={option.image}
|
|
124
133
|
alt={option.label}
|
|
125
134
|
getImageUrl={getImageUrl}
|
|
126
|
-
size={16}
|
|
135
|
+
size={option.subtitle ? 22 : 16}
|
|
127
136
|
/>
|
|
128
137
|
) : (
|
|
129
138
|
option.icon && <DynamicIcon name={option.icon} className="h-3.5 w-3.5" />
|
|
130
139
|
)}
|
|
131
|
-
|
|
140
|
+
{option.subtitle ? (
|
|
141
|
+
// Two-line identity: label on top, the secondary identifier
|
|
142
|
+
// (SKU/email/…) muted underneath. `leading-tight` keeps the pill
|
|
143
|
+
// compact; the subtitle inherits the badge's fg at reduced opacity
|
|
144
|
+
// so it reads as secondary on any color.
|
|
145
|
+
<span className="flex flex-col leading-tight text-start">
|
|
146
|
+
<span>{option.label}</span>
|
|
147
|
+
<span className="text-[0.7rem] opacity-70">{option.subtitle}</span>
|
|
148
|
+
</span>
|
|
149
|
+
) : (
|
|
150
|
+
<span>{option.label}</span>
|
|
151
|
+
)}
|
|
132
152
|
</Badge>
|
|
133
153
|
)
|
|
134
154
|
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -332,7 +332,10 @@ const BadgeWithEndpointOptions: React.FC<{
|
|
|
332
332
|
const { optionsMap } = React.useContext(OptionsContext)
|
|
333
333
|
const options = optionsMap.get(endpoint) || []
|
|
334
334
|
const option = options.find((opt: any) => opt.value === value)
|
|
335
|
-
|
|
335
|
+
// Reference options carry the backend-projected `description` (the SKU/email
|
|
336
|
+
// the author pointed the options config at) — surface it as the chip subtitle
|
|
337
|
+
// so a resolved record reads "Name / SKU" instead of a bare name.
|
|
338
|
+
if (option) return <OptionBadge option={{ ...option, subtitle: option.subtitle ?? (option as any).description }} fallback={String(value)} getImageUrl={getImageUrl} />
|
|
336
339
|
// No declared option matched → humanize the raw token as a safety net so a
|
|
337
340
|
// cell never shows `in_progress` verbatim (option.label still wins above).
|
|
338
341
|
return <Badge variant="outline">{humanizeToken(value)}</Badge>
|
|
@@ -461,6 +464,23 @@ export const resolveRelationImage = (col: ColumnDefinition, row: any): string =>
|
|
|
461
464
|
return ''
|
|
462
465
|
}
|
|
463
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Reads a secondary identifier the backend stamps on a resolved FK sibling — a
|
|
469
|
+
* product's SKU, a user's email — projected as `subtitle`/`description` (the
|
|
470
|
+
* relational twin of `image` via the column's `label_description`). Rendered
|
|
471
|
+
* muted under the label so a reference chip reads "Name / SKU". Domain-agnostic:
|
|
472
|
+
* only the generic `subtitle`/`description` keys are read (never a hardcoded
|
|
473
|
+
* `sku`/`code`), so the author picks the column declaratively. '' when absent.
|
|
474
|
+
*/
|
|
475
|
+
export const resolveRelationSubtitle = (col: ColumnDefinition, row: any): string => {
|
|
476
|
+
const sibling = getNestedValue(row, relationKeyFor(col))
|
|
477
|
+
if (sibling && typeof sibling === 'object') {
|
|
478
|
+
const sub = sibling.subtitle ?? sibling.description
|
|
479
|
+
if (sub !== undefined && sub !== null && sub !== '') return String(sub)
|
|
480
|
+
}
|
|
481
|
+
return ''
|
|
482
|
+
}
|
|
483
|
+
|
|
464
484
|
/**
|
|
465
485
|
* Renders a resolved FK relation as a clean, truncated chip. Reads the
|
|
466
486
|
* backend-resolved sibling `{ value, label[, image] }` (see `relationKeyFor`)
|
|
@@ -478,6 +498,7 @@ const RelationCell: React.FC<{
|
|
|
478
498
|
const display = resolveRelationLabel(col, row)
|
|
479
499
|
if (!display) return <EmptyCell />
|
|
480
500
|
const image = resolveRelationImage(col, row)
|
|
501
|
+
const subtitle = resolveRelationSubtitle(col, row)
|
|
481
502
|
// Deterministic, SUBTLE color keyed on the resolved label — lighter than
|
|
482
503
|
// enum badges (soft tint, no heavy fill) so category/brand chips read as
|
|
483
504
|
// alive yet stay visually distinct from option/status badges. Inline style
|
|
@@ -487,12 +508,19 @@ const RelationCell: React.FC<{
|
|
|
487
508
|
<span
|
|
488
509
|
className="inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-sm font-medium"
|
|
489
510
|
style={chipStyles}
|
|
490
|
-
title={display}
|
|
511
|
+
title={subtitle ? `${display} · ${subtitle}` : display}
|
|
491
512
|
>
|
|
492
513
|
{image && (
|
|
493
|
-
<RelationThumbnail src={image} alt={display} getImageUrl={getImageUrl} size={18} />
|
|
514
|
+
<RelationThumbnail src={image} alt={display} getImageUrl={getImageUrl} size={subtitle ? 24 : 18} />
|
|
515
|
+
)}
|
|
516
|
+
{subtitle ? (
|
|
517
|
+
<span className="flex flex-col leading-tight min-w-0">
|
|
518
|
+
<span className="truncate">{display}</span>
|
|
519
|
+
<span className="truncate text-[0.7rem] opacity-70">{subtitle}</span>
|
|
520
|
+
</span>
|
|
521
|
+
) : (
|
|
522
|
+
<span className="truncate">{display}</span>
|
|
494
523
|
)}
|
|
495
|
-
<span className="truncate">{display}</span>
|
|
496
524
|
</span>
|
|
497
525
|
)
|
|
498
526
|
}
|