@asteby/metacore-runtime-react 23.1.0 → 23.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/action-modal-dispatcher.js +4 -4
  3. package/dist/dialogs/dynamic-record.d.ts +5 -0
  4. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  5. package/dist/dialogs/dynamic-record.js +146 -31
  6. package/dist/display-value.d.ts +56 -0
  7. package/dist/display-value.d.ts.map +1 -0
  8. package/dist/display-value.js +74 -0
  9. package/dist/dynamic-columns.d.ts.map +1 -1
  10. package/dist/dynamic-columns.js +2 -53
  11. package/dist/dynamic-kanban.d.ts +19 -3
  12. package/dist/dynamic-kanban.d.ts.map +1 -1
  13. package/dist/dynamic-kanban.js +114 -8
  14. package/dist/dynamic-row-actions.d.ts.map +1 -1
  15. package/dist/dynamic-row-actions.js +5 -3
  16. package/dist/dynamic-table.d.ts +9 -1
  17. package/dist/dynamic-table.d.ts.map +1 -1
  18. package/dist/dynamic-table.js +132 -32
  19. package/dist/use-infinite-scroll.d.ts +27 -0
  20. package/dist/use-infinite-scroll.d.ts.map +1 -0
  21. package/dist/use-infinite-scroll.js +60 -0
  22. package/package.json +3 -3
  23. package/src/__tests__/dynamic-kanban-infinite-scroll.test.tsx +210 -0
  24. package/src/__tests__/dynamic-table-infinite-scroll.test.tsx +196 -0
  25. package/src/__tests__/readonly-fields.test.tsx +97 -0
  26. package/src/__tests__/record-detail-display.test.tsx +113 -0
  27. package/src/__tests__/use-infinite-scroll.test.tsx +147 -0
  28. package/src/action-modal-dispatcher.tsx +4 -4
  29. package/src/dialogs/dynamic-record.tsx +217 -40
  30. package/src/display-value.tsx +134 -0
  31. package/src/dynamic-columns.tsx +6 -102
  32. package/src/dynamic-kanban.tsx +168 -8
  33. package/src/dynamic-row-actions.tsx +5 -3
  34. package/src/dynamic-table.tsx +149 -10
  35. package/src/use-infinite-scroll.ts +94 -0
@@ -19,9 +19,10 @@ import * as icons from 'lucide-react';
19
19
  import { MoreHorizontal } from 'lucide-react';
20
20
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
21
21
  import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
22
- import { generateBadgeStyles, getInitials, optionColor, relationChipStyles, } from '@asteby/metacore-ui/lib';
22
+ import { generateBadgeStyles, getInitials, relationChipStyles, } from '@asteby/metacore-ui/lib';
23
23
  import { Progress } from './dialogs/_primitives';
24
24
  import { humanizeToken } from './dynamic-columns-helpers';
25
+ import { OptionBadge, RelationThumbnail, statusColorFor, useIsDarkTheme, } from './display-value';
25
26
  import { OptionsContext } from './options-context';
26
27
  import { DynamicIcon, isLucideIconName } from './dynamic-icon';
27
28
  import { CollectionCell } from './collection-cell';
@@ -88,22 +89,6 @@ export const formatAggregateTotal = (col, value, currency, locale) => {
88
89
  ? { minimumFractionDigits: decimals, maximumFractionDigits: decimals }
89
90
  : {}, locale);
90
91
  };
91
- /**
92
- * Semantic status → badge color. Used by the `status` cell when no explicit
93
- * `options` color is declared. Generic, value-driven mapping.
94
- */
95
- const statusColorFor = (value) => {
96
- const v = value.toLowerCase();
97
- if (['active', 'enabled', 'paid', 'completed', 'done', 'success', 'approved', 'open']
98
- .includes(v))
99
- return '#22c55e';
100
- if (['pending', 'draft', 'processing', 'in_progress', 'review', 'waiting'].includes(v))
101
- return '#eab308';
102
- if (['inactive', 'disabled', 'cancelled', 'canceled', 'failed', 'rejected', 'error', 'closed']
103
- .includes(v))
104
- return '#ef4444';
105
- return '#6b7280';
106
- };
107
92
  /** Copyable monospaced text cell (code/IDs/hashes). */
108
93
  const CodeCell = ({ text, maxLength }) => {
109
94
  const [copied, setCopied] = React.useState(false);
@@ -195,23 +180,6 @@ const getValueFromPathVariants = (obj, path) => {
195
180
  }
196
181
  return undefined;
197
182
  };
198
- const useIsDarkTheme = () => {
199
- const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
200
- document.documentElement.classList.contains('dark'));
201
- React.useEffect(() => {
202
- if (typeof document === 'undefined')
203
- return;
204
- const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
205
- sync();
206
- const observer = new MutationObserver(sync);
207
- observer.observe(document.documentElement, {
208
- attributes: true,
209
- attributeFilter: ['class'],
210
- });
211
- return () => observer.disconnect();
212
- }, []);
213
- return isDark;
214
- };
215
183
  const renderRelationBadges = (items, col) => {
216
184
  if (!Array.isArray(items) || items.length === 0) {
217
185
  return _jsx("span", { className: "text-muted-foreground", children: "-" });
@@ -240,25 +208,6 @@ const renderRelationBadges = (items, col) => {
240
208
  return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1", children: [iconValue && (_jsx(DynamicIcon, { name: iconValue, className: "h-3 w-3" })), _jsx("span", { children: label })] }, `${col.key}-${idx}`));
241
209
  }) }));
242
210
  };
243
- /**
244
- * Tiny square thumbnail for a resolved relation/option that carries an `image`
245
- * (brand logo, product photo, customer/user avatar). Uses the same Avatar
246
- * primitive as the `avatar`/`creator` cells so a broken/loading image
247
- * gracefully falls back to the record's initials. Sized small (the box is an
248
- * inline style so an addon-arbitrary Tailwind class never gets dropped by a
249
- * consuming app's class scan). Rendered inline alongside a label — never alone.
250
- */
251
- const RelationThumbnail = ({ src, alt, getImageUrl, size = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
252
- const OptionBadge = ({ option, getImageUrl }) => {
253
- const isDark = useIsDarkTheme();
254
- // Explicit backend color wins; otherwise derive a stable, cohesive color
255
- // from the option's value (fallback label) so "dead" gray badges come
256
- // alive. Inline style (hex-derived) so it works regardless of the host's
257
- // tailwind safelist — addon-arbitrary classes aren't in the host scan.
258
- const colorSource = option.color || optionColor(option.value || option.label);
259
- const colorStyles = generateBadgeStyles(colorSource, { isDark });
260
- 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 })] }));
261
- };
262
211
  const BadgeWithEndpointOptions = ({ endpoint, value, getImageUrl }) => {
263
212
  const { optionsMap } = React.useContext(OptionsContext);
264
213
  const options = optionsMap.get(endpoint) || [];
@@ -32,6 +32,16 @@ export declare function isTransitionAllowed(transitions: StageTransition[] | und
32
32
  * resolves, and so the previous grouping can be restored on failure.
33
33
  */
34
34
  export declare function applyOptimisticMove(grouped: Map<string, any[]>, cardId: string | number, fromStage: string, toStage: string, groupByKey: string): Map<string, any[]>;
35
+ /**
36
+ * Returns a NEW per-lane pagination map with the server totals adjusted for a
37
+ * card moving `fromStage` → `toStage`: the source loses one, the destination
38
+ * gains one. Lanes whose `total` is still unknown (`null`, not yet topped up)
39
+ * are left alone. Pure — backs the optimistic drag so a partial lane's
40
+ * `count/total` header stays truthful, and can be restored on PUT failure.
41
+ */
42
+ export declare function applyLaneTotalsOnMove<T extends {
43
+ total: number | null;
44
+ }>(pagination: Record<string, T>, fromStage: string, toStage: string): Record<string, T>;
35
45
  /**
36
46
  * Picks the columns shown on a card: a `title` column (first searchable column,
37
47
  * else first text-ish column) and up to `maxFields` secondary columns. Excludes
@@ -88,10 +98,16 @@ export interface DynamicKanbanProps {
88
98
  */
89
99
  onAction?: (action: string, row: any) => void;
90
100
  /**
91
- * Max cards fetched per lane render. Kanban shows all cards at once (no
92
- * pagination UI), so it requests a single large page. Defaults to 200.
101
+ * Size of the INITIAL board page (one request, grouped into lanes). Each
102
+ * lane then tops up incrementally on scroll (see `lanePageSize`). Defaults
103
+ * to 50 — enough to fill the visible lanes without loading the whole board.
93
104
  */
94
105
  pageSize?: number;
106
+ /**
107
+ * Page size for a lane's incremental top-up fetch (scoped by
108
+ * `f_<group_by>=<stage>`). Defaults to 25.
109
+ */
110
+ lanePageSize?: number;
95
111
  /** IANA timezone for datetime card fields (org config). */
96
112
  timeZone?: string;
97
113
  /** ISO 4217 currency for money card fields (org config). */
@@ -102,5 +118,5 @@ export interface DynamicKanbanProps {
102
118
  */
103
119
  defaultFilters?: Record<string, any>;
104
120
  }
105
- export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize, timeZone, currency, defaultFilters, }: DynamicKanbanProps): React.JSX.Element;
121
+ export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize, lanePageSize, timeZone, currency, defaultFilters, }: DynamicKanbanProps): React.JSX.Element;
106
122
  //# sourceMappingURL=dynamic-kanban.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
10
10
  import { useApi } from './api-context';
11
11
  import { useDynamicFilters } from './use-dynamic-filters';
12
12
  import { FilterChipsRow, summarizeFilterValues, translateOptionLabels, } from './filter-chips';
13
+ import { dedupeById, useInfiniteScrollSentinel } from './use-infinite-scroll';
13
14
  import { useMetadataCache } from './metadata-cache';
14
15
  import { ActivityValueRenderer } from './activity-value-renderer';
15
16
  import { DynamicIcon } from './dynamic-icon';
@@ -112,6 +113,25 @@ export function applyOptimisticMove(grouped, cardId, fromStage, toStage, groupBy
112
113
  next.set(toStage, toRows);
113
114
  return next;
114
115
  }
116
+ /**
117
+ * Returns a NEW per-lane pagination map with the server totals adjusted for a
118
+ * card moving `fromStage` → `toStage`: the source loses one, the destination
119
+ * gains one. Lanes whose `total` is still unknown (`null`, not yet topped up)
120
+ * are left alone. Pure — backs the optimistic drag so a partial lane's
121
+ * `count/total` header stays truthful, and can be restored on PUT failure.
122
+ */
123
+ export function applyLaneTotalsOnMove(pagination, fromStage, toStage) {
124
+ const next = { ...pagination };
125
+ const bump = (key, delta) => {
126
+ const st = next[key];
127
+ if (st && st.total != null) {
128
+ next[key] = { ...st, total: Math.max(0, st.total + delta) };
129
+ }
130
+ };
131
+ bump(fromStage, -1);
132
+ bump(toStage, +1);
133
+ return next;
134
+ }
115
135
  /**
116
136
  * Picks the columns shown on a card: a `title` column (first searchable column,
117
137
  * else first text-ish column) and up to `maxFields` secondary columns. Excludes
@@ -195,7 +215,7 @@ function useIsDarkTheme() {
195
215
  }, []);
196
216
  return isDark;
197
217
  }
198
- export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize = 200, timeZone, currency, defaultFilters, }) {
218
+ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize = 50, lanePageSize = 25, timeZone, currency, defaultFilters, }) {
199
219
  const { t, i18n } = useTranslation();
200
220
  const api = useApi();
201
221
  const isDark = useIsDarkTheme();
@@ -205,6 +225,12 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
205
225
  const [records, setRecords] = useState([]);
206
226
  const [loading, setLoading] = useState(!cachedMeta);
207
227
  const [loadingData, setLoadingData] = useState(true);
228
+ // Per-stage incremental pagination for infinite scroll. The initial board
229
+ // page (grouped into lanes) is fetched once; each lane then tops up its OWN
230
+ // stage via `f_<group_by>=<stage>&page=n`, appended (deduped by id) into the
231
+ // shared `records`. `total` is the stage's server count when the response
232
+ // meta carries it. Reset whenever the filters/search change.
233
+ const [lanePagination, setLanePagination] = useState({});
208
234
  // Active drag card id (for the DragOverlay + drop-zone highlighting).
209
235
  const [activeId, setActiveId] = useState(null);
210
236
  const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
@@ -247,7 +273,10 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
247
273
  // and `f_<key>` serialization DynamicTable uses, so the board filters
248
274
  // identically to its table sibling.
249
275
  const { dynamicFilters, globalFilter, setGlobalFilter, columnFilterConfigs, filterParams, activeFilterCount, handleDynamicFilterChange, clearAll, } = useDynamicFilters(metadata, { defaultFilters, model, endpoint });
250
- // ---- records fetch (same path as DynamicTable, single large page) ----
276
+ // ---- initial board page (one request, grouped into lanes) ----
277
+ // Resets the per-lane pagination so every lane restarts its incremental
278
+ // top-up from scratch — called on mount, refresh, and any filter/search
279
+ // change (fetchData's identity changes with filterParams).
251
280
  const fetchData = useCallback(async () => {
252
281
  if (!metadata)
253
282
  return;
@@ -265,7 +294,64 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
265
294
  finally {
266
295
  setLoadingData(false);
267
296
  }
297
+ setLanePagination({});
268
298
  }, [api, endpoint, model, metadata, pageSize, filterParams]);
299
+ // Load the next page for ONE lane/stage and append it (deduped by id) into
300
+ // the shared records. Scoped by `f_<group_by>=<stage>` on top of the active
301
+ // filterParams (the stage scope wins over any global group_by filter).
302
+ const groupByKey = metadata?.group_by || '';
303
+ const loadMoreLane = useCallback(async (stageKey) => {
304
+ if (!metadata || !groupByKey)
305
+ return;
306
+ const current = lanePagination[stageKey];
307
+ if (current?.loading || current?.done)
308
+ return;
309
+ const nextPage = current?.nextPage ?? 1;
310
+ setLanePagination((p) => ({
311
+ ...p,
312
+ [stageKey]: {
313
+ nextPage,
314
+ total: current?.total ?? null,
315
+ loading: true,
316
+ done: false,
317
+ },
318
+ }));
319
+ try {
320
+ const res = (await api.get(endpoint || `/data/${model}`, {
321
+ params: {
322
+ ...filterParams,
323
+ page: nextPage,
324
+ per_page: lanePageSize,
325
+ [`f_${groupByKey}`]: stageKey,
326
+ },
327
+ }));
328
+ const rows = res.data.success ? res.data.data || [] : [];
329
+ const total = res.data.meta?.total ?? res.data.meta?.count ?? null;
330
+ setRecords((prev) => dedupeById(prev, rows));
331
+ setLanePagination((p) => ({
332
+ ...p,
333
+ [stageKey]: {
334
+ nextPage: nextPage + 1,
335
+ total,
336
+ loading: false,
337
+ // Exhausted when the server returned a short page.
338
+ done: rows.length < lanePageSize,
339
+ },
340
+ }));
341
+ }
342
+ catch (err) {
343
+ console.error(`Error al cargar más tarjetas de ${stageKey}`, err);
344
+ setLanePagination((p) => ({
345
+ ...p,
346
+ [stageKey]: {
347
+ nextPage,
348
+ total: current?.total ?? null,
349
+ loading: false,
350
+ done: false,
351
+ },
352
+ }));
353
+ }
354
+ }, [api, endpoint, model, metadata, groupByKey, filterParams, lanePageSize, lanePagination]);
269
355
  // Refetch when metadata resolves, on an explicit refresh, or when the
270
356
  // filters change. `fetchData` is stable while `filterParams` is unchanged
271
357
  // (both memoized), so this only re-runs on real input changes. Debounced so
@@ -348,7 +434,6 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
348
434
  });
349
435
  }, []);
350
436
  const stages = useMemo(() => (metadata ? deriveStages(metadata) : []), [metadata]);
351
- const groupByKey = metadata?.group_by || '';
352
437
  const transitions = metadata?.transitions;
353
438
  const grouped = useMemo(() => groupByStage(records, groupByKey, stages), [records, groupByKey, stages]);
354
439
  const { title: titleCol, fields: fieldCols } = useMemo(() => (metadata ? selectCardColumns(metadata) : { title: null, fields: [] }), [metadata]);
@@ -405,7 +490,12 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
405
490
  }
406
491
  // OPTIMISTIC: move the card in local state immediately.
407
492
  const prevRecords = records;
493
+ const prevPagination = lanePagination;
408
494
  setRecords((rs) => rs.map((r) => String(r.id) === cardId ? { ...r, [groupByKey]: destStage } : r));
495
+ // Keep the server totals consistent with the moved card so a lane's
496
+ // `count/total` header stays truthful with partial lanes: one leaves
497
+ // the source stage, one joins the destination.
498
+ setLanePagination((p) => applyLaneTotalsOnMove(p, srcStage, destStage));
409
499
  try {
410
500
  const base = endpoint || `/data/${model}`;
411
501
  // `base` is the org-scoped list endpoint (e.g. `/data/<model>/me`),
@@ -422,6 +512,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
422
512
  catch (err) {
423
513
  // REVERT + toast on failure.
424
514
  setRecords(prevRecords);
515
+ setLanePagination(prevPagination);
425
516
  toast.error(t('kanban.moveFailed', {
426
517
  defaultValue: 'No se pudo mover la tarjeta',
427
518
  }) +
@@ -429,7 +520,7 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
429
520
  ? `: ${err.response.data.message}`
430
521
  : ''));
431
522
  }
432
- }, [api, endpoint, groupByKey, model, records, stageOfCard, t, transitions]);
523
+ }, [api, endpoint, groupByKey, lanePagination, model, records, stageOfCard, t, transitions]);
433
524
  if (loading) {
434
525
  return (_jsx("div", { className: "flex gap-4 overflow-x-auto p-1", children: [0, 1, 2, 3].map((i) => (_jsxs("div", { className: "w-[300px] shrink-0 space-y-3", children: [_jsx(Skeleton, { className: "h-8 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" }), _jsx(Skeleton, { className: "h-24 w-full" })] }, i))) }));
435
526
  }
@@ -474,7 +565,12 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
474
565
  const droppableAllowed = !activeId ||
475
566
  stage.key === activeStage ||
476
567
  isTransitionAllowed(transitions, activeStage, stage.key);
477
- return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, filterFields: filterFields, laneFilter: laneFilter, onFunnelChange: (f) => updateLaneFilter(stage.key, {
568
+ // Infinite scroll is per declared stage; the synthetic
569
+ // "unassigned" lane can't be stage-scoped, so it never tops up.
570
+ const laneState = lanePagination[stage.key];
571
+ const isUnassigned = stage.key === UNASSIGNED_LANE;
572
+ const laneHasMore = !isUnassigned && !laneState?.done;
573
+ return (_jsx(KanbanLane, { stage: stage, count: cards.length, totalCount: allCards.length, serverTotal: laneState?.total ?? null, hasMore: laneHasMore, loadingMore: !!laneState?.loading, onLoadMore: () => loadMoreLane(stage.key), filterFields: filterFields, laneFilter: laneFilter, onFunnelChange: (f) => updateLaneFilter(stage.key, {
478
574
  field: f?.field,
479
575
  values: f?.values,
480
576
  text: f?.text,
@@ -508,9 +604,15 @@ function SheetFilterRow({ field, isStage, }) {
508
604
  const summary = summarizeFilterValues(field.config.selectedValues, field.config.options);
509
605
  return (_jsx(ColumnFilterControl, { variant: "row", align: "end", icon: filterTypeIcon(field.config.filterType, isStage), label: field.label, valueSummary: summary, filterKey: field.config.filterKey, filterType: field.config.filterType, filterOptions: field.config.options, filterLoading: field.config.loading, filterSearchEndpoint: field.config.searchEndpoint, selectedValues: field.config.selectedValues, onFilterChange: field.config.onFilterChange, loadOptions: field.config.loadOptions }));
510
606
  }
511
- function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, disabled, children, }) {
607
+ function KanbanLane({ stage, count, totalCount, serverTotal, hasMore, loadingMore, onLoadMore, filterFields, laneFilter, onFunnelChange, onQueryChange, isDark, dimmed, disabled, children, }) {
512
608
  const { t } = useTranslation();
513
609
  const { setNodeRef, isOver } = useDroppable({ id: stage.key, disabled });
610
+ // Infinite scroll: the sentinel lives at the bottom of the lane's own scroll
611
+ // container; a load in flight or an exhausted stage disables it.
612
+ const { rootRef, sentinelRef } = useInfiniteScrollSentinel({
613
+ onLoadMore,
614
+ disabled: !hasMore || loadingMore,
615
+ });
514
616
  const headerStyle = generateBadgeStyles(stage.color || optionColor(stage.key), {
515
617
  isDark,
516
618
  });
@@ -546,7 +648,11 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
546
648
  opacity: dimmed ? 0.45 : 1,
547
649
  outline: isOver && !disabled ? '2px solid var(--ring, #3b82f6)' : 'none',
548
650
  outlineOffset: 2,
549
- }, "data-stage": stage.key, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: laneActive ? `${count}/${totalCount}` : count })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${queryActive ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.searchLane', {
651
+ }, "data-stage": stage.key, "data-disabled": disabled || undefined, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2.5", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Badge, { variant: "outline", className: "border-0 text-xs font-semibold", style: headerStyle, children: t(stage.label, { defaultValue: stage.label }) }), _jsx("span", { className: "text-xs font-medium tabular-nums text-muted-foreground", children: laneActive
652
+ ? `${count}/${totalCount}`
653
+ : serverTotal != null
654
+ ? `${count}/${serverTotal}`
655
+ : count })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs("button", { type: "button", onClick: () => setSearchOpen((o) => !o), className: `relative flex size-6 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-foreground ${queryActive ? 'text-primary' : 'text-muted-foreground'}`, "aria-label": t('kanban.searchLane', {
550
656
  defaultValue: 'Buscar en la columna',
551
657
  }), children: [_jsx(Search, { className: "h-3.5 w-3.5" }), queryActive && (_jsx("span", { className: "absolute -right-0.5 -top-0.5 size-1.5 rounded-full bg-primary" }))] }), _jsx(LaneFilterButton, { fields: filterFields, value: funnelValue, onChange: onFunnelChange })] })] }), searchOpen && (_jsx("div", { className: "px-3 pb-1.5", children: _jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { ref: searchRef, value: laneFilter?.query ?? '', onChange: (e) => onQueryChange(e.target.value), onKeyDown: (e) => {
552
658
  if (e.key === 'Escape') {
@@ -560,7 +666,7 @@ function KanbanLane({ stage, count, totalCount, filterFields, laneFilter, onFunn
560
666
  defaultValue: 'Buscar tarjetas...',
561
667
  }), className: "h-7 pl-7 text-xs" })] }) })), funnelActive && (_jsxs("div", { className: "flex items-center gap-1 px-3 pb-1.5 text-[11px] text-muted-foreground", children: [_jsx(ListFilter, { className: "h-3 w-3 shrink-0" }), _jsxs("span", { className: "truncate", children: [activeFieldLabel, ": ", funnelSummary] }), _jsx("button", { type: "button", onClick: () => onFunnelChange(null), className: "ml-auto rounded p-0.5 hover:bg-muted", "aria-label": t('kanban.clearFilters', {
562
668
  defaultValue: 'Limpiar',
563
- }), children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsx("div", { className: "flex min-h-[55vh] max-h-[70vh] min-w-0 flex-col gap-2 overflow-y-auto px-2 pb-3", children: children })] }));
669
+ }), children: _jsx(X, { className: "h-3 w-3" }) })] })), _jsxs("div", { ref: rootRef, className: "flex min-h-[55vh] max-h-[70vh] min-w-0 flex-col gap-2 overflow-y-auto px-2 pb-3", children: [children, loadingMore && (_jsx(Skeleton, { className: "h-16 w-full shrink-0", "data-testid": "lane-loading-more" })), hasMore && (_jsx("div", { ref: sentinelRef, className: "h-1 w-full shrink-0", "aria-hidden": true }))] })] }));
564
670
  }
565
671
  // LaneFilterButton — the per-column funnel. Picks a field + a value and narrows
566
672
  // ONLY this lane's cards (client-side, in the parent). Draft state lives here so
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,SAAS,CAAA;AAE5D,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CAmHhD"}
1
+ {"version":3,"file":"dynamic-row-actions.d.ts","sourceRoot":"","sources":["../src/dynamic-row-actions.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,SAAS,CAAA;AAE5D,MAAM,WAAW,0BAA0B;IACvC,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,4EAA4E;IAC5E,SAAS,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,iBAAiB;IAC9B,+CAA+C;IAC/C,oBAAoB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC,YAAY,CAAA;CAC9B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,GACZ,EAAE,0BAA0B,GAAG,iBAAiB,CAqHhD"}
@@ -89,16 +89,18 @@ export function useDynamicRowActions({ model, endpoint, metadata, onAction, onRe
89
89
  try {
90
90
  const deleteEndpoint = endpoint ? `${endpoint}/${rowToDelete.id}` : `/data/${model}/${rowToDelete.id}`;
91
91
  const res = await api.delete(deleteEndpoint);
92
+ // CRUD estándar: no usar res.data.message (el endpoint dinámico
93
+ // devuelve texto en inglés que se filtraría al toast). String localizado.
92
94
  if (res.data.success) {
93
- toast.success(res.data.message || 'Eliminado correctamente');
95
+ toast.success(t('dynamic.delete_success', { defaultValue: 'Registro eliminado correctamente' }));
94
96
  onRefresh();
95
97
  }
96
98
  else
97
- toast.error(res.data.message || 'Error al eliminar');
99
+ toast.error(t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
98
100
  }
99
101
  catch (error) {
100
102
  console.error('Error al eliminar', error);
101
- toast.error('Error al eliminar el registro');
103
+ toast.error(t('dynamic.delete_error', { defaultValue: 'No se pudo eliminar el registro' }));
102
104
  }
103
105
  finally {
104
106
  setIsDeleting(false);
@@ -36,6 +36,14 @@ export interface DynamicTableProps {
36
36
  * an explicit per-column currency. Optional — defaults to 'USD'.
37
37
  */
38
38
  currency?: string;
39
+ /**
40
+ * Opt into infinite scroll: instead of the classic pager, rows accumulate as
41
+ * the user scrolls (a sentinel at the bottom fetches + appends the next
42
+ * page, deduped by id, respecting the active filters/search). Changing any
43
+ * filter/sort/search resets to page 1. Default false — existing hosts keep
44
+ * the classic pagination untouched.
45
+ */
46
+ infiniteScroll?: boolean;
39
47
  }
40
- export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, }: DynamicTableProps): import("react").JSX.Element;
48
+ export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, infiniteScroll, }: DynamicTableProps): import("react").JSX.Element;
41
49
  //# sourceMappingURL=dynamic-table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAYnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAw6BnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAanF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,cAAsB,GACzB,EAAE,iBAAiB,+BAyiCnB"}