@asteby/metacore-runtime-react 6.4.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 7.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0cd085c: Zero-config CRUD UX from a single column flag.
8
+
9
+ Three changes that move polish from each app's metadata into the SDK default behaviour, so a model only needs `enableCRUDActions: true` plus `filterable: true` on the columns it wants searchable to get the same UX link / ops / hub render today:
10
+ 1. **Auto-derive filter chip type from column type.** A column flagged `filterable: true` without options or a `searchEndpoint` no longer falls back to "no filter" — it picks the FilterableColumnHeader variant that matches the column type: `text` for text/email/phone/tags, `number_range` for numeric columns, `boolean` for booleans, `select` when options/endpoint are present.
11
+ 2. **Auto-render the row Actions column when `enableCRUDActions` is on.** If the host metadata already declares its own `actions[]`, those win. When it doesn't, the SDK falls back to the canonical View / Edit / Delete trio wired to DynamicTable's existing `view` / `edit` / `delete` handlers — no host-side glue.
12
+ 3. **`<DynamicCRUDPage>` defaults `hideRefresh` to `true`.** The page-level Refresh button duplicated the one DynamicTable's internal toolbar already ships next to "View"; the page chrome now defers to it. Apps that want both back can pass `hideRefresh={false}`.
13
+
14
+ ## 7.0.0
15
+
16
+ ### Patch Changes
17
+
18
+ - 3450876: Add `getInitials(name)` helper to `@asteby/metacore-ui/lib`.
19
+
20
+ Pulls a duplicated 6-line snippet (`name.split(' ').map(n => n[0]).slice(0, 2).join('').toUpperCase()`) out of every avatar across the platform — chat headers, profile dropdowns, dynamic-table avatar cells, sidebar nav. Trims whitespace, caps token count, and falls back to a single character when the input is empty.
21
+
22
+ `runtime-react`'s avatar cell renderer now uses it; visually identical, one less inline lambda.
23
+
24
+ - Updated dependencies [3450876]
25
+ - @asteby/metacore-ui@0.7.0
26
+
3
27
  ## 6.4.0
4
28
 
5
29
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,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;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAmVnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,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;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAiXnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -15,7 +15,7 @@ import * as icons from 'lucide-react';
15
15
  import { MoreHorizontal } from 'lucide-react';
16
16
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
17
17
  import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
18
- import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
18
+ import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib';
19
19
  import { OptionsContext } from './options-context';
20
20
  import { DynamicIcon } from './dynamic-icon';
21
21
  const defaultGetImageUrl = (path) => path;
@@ -207,12 +207,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
207
207
  else if (value) {
208
208
  avatarSrc = `${apiBaseUrl}${col.basePath || ''}${value}`;
209
209
  }
210
- return (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg ring-1 ring-border/50", children: [_jsx(AvatarImage, { src: getImageUrl(avatarSrc || ''), alt: String(name), className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: String(name)
211
- .split(' ')
212
- .map((n) => n[0])
213
- .slice(0, 2)
214
- .join('')
215
- .toUpperCase() })] }), _jsxs("div", { className: "flex flex-col min-w-0 overflow-hidden", children: [_jsx("span", { className: "font-medium text-sm truncate leading-none mb-0.5 text-foreground/90", children: String(name) }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: String(desc) }))] })] }));
210
+ return (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg ring-1 ring-border/50", children: [_jsx(AvatarImage, { src: getImageUrl(avatarSrc || ''), alt: String(name), className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: getInitials(String(name)) })] }), _jsxs("div", { className: "flex flex-col min-w-0 overflow-hidden", children: [_jsx("span", { className: "font-medium text-sm truncate leading-none mb-0.5 text-foreground/90", children: String(name) }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: String(desc) }))] })] }));
216
211
  }
217
212
  case 'relation-badge-list':
218
213
  return renderRelationBadges(value, col);
@@ -271,14 +266,47 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
271
266
  enableHiding: true,
272
267
  });
273
268
  });
274
- if (metadata.hasActions && metadata.actions.length > 0) {
269
+ // Resolve which actions to surface in the row dropdown:
270
+ // 1. If the host metadata declares its own actions, use them as-is.
271
+ // 2. Otherwise, when enableCRUDActions is true, fall back to the
272
+ // canonical View / Edit / Delete trio so any model with CRUD on
273
+ // gets the same dropdown without the host having to declare it.
274
+ // The DynamicTable wires `view`/`edit`/`delete` to its own dialogs
275
+ // through onAction, so labels/icons are the only thing this needs to
276
+ // ship.
277
+ const explicitActions = metadata.actions ?? [];
278
+ const hasExplicitActions = (metadata.hasActions ?? explicitActions.length > 0) && explicitActions.length > 0;
279
+ const defaultCRUDActions = metadata.enableCRUDActions
280
+ ? [
281
+ {
282
+ key: 'view',
283
+ name: 'view',
284
+ label: t ? t('datatable.view_record') : 'Ver',
285
+ icon: 'Eye',
286
+ },
287
+ {
288
+ key: 'edit',
289
+ name: 'edit',
290
+ label: t ? t('datatable.edit') : 'Editar',
291
+ icon: 'Pencil',
292
+ },
293
+ {
294
+ key: 'delete',
295
+ name: 'delete',
296
+ label: t ? t('datatable.delete') : 'Eliminar',
297
+ icon: 'Trash2',
298
+ },
299
+ ]
300
+ : [];
301
+ const resolvedActions = hasExplicitActions ? explicitActions : defaultCRUDActions;
302
+ if (resolvedActions.length > 0) {
275
303
  columns.push({
276
304
  id: 'actions',
277
305
  header: () => _jsx("div", { className: "text-right", children: t ? t('common.actions') : 'Acciones' }),
278
306
  size: 80,
279
307
  maxSize: 80,
280
308
  meta: {},
281
- cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: metadata.actions
309
+ cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: resolvedActions
282
310
  .filter((action) => {
283
311
  if (!action.condition)
284
312
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAWd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAkL1D"}
1
+ {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAWd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAsL1D"}
@@ -87,7 +87,11 @@ export function DynamicCRUDPage(props) {
87
87
  const effectiveHideCreate = hideCreate || ext?.hideCreate;
88
88
  const effectiveHideExport = hideExport || ext?.hideExport;
89
89
  const effectiveHideImport = hideImport || ext?.hideImport;
90
- const effectiveHideRefresh = hideRefresh || ext?.hideRefresh;
90
+ // Refresh defaults to hidden in the page header — <DynamicTable> ships
91
+ // its own refresh icon next to the View / column-visibility toolbar, so
92
+ // showing one in the page chrome is just visual duplication. Apps that
93
+ // want it back can pass `hideRefresh={false}`.
94
+ const effectiveHideRefresh = hideRefresh ?? ext?.hideRefresh ?? true;
91
95
  const showCreate = enableCRUD && !effectiveHideCreate;
92
96
  const showImport = enableCRUD && !effectiveHideImport;
93
97
  const showExport = !effectiveHideExport;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AASnF,UAAU,iBAAiB;IACvB,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,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;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CA4qBnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AASnF,UAAU,iBAAiB;IACvB,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,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;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CAsrBnB"}
@@ -469,8 +469,20 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
469
469
  continue;
470
470
  const hasStaticOptions = (c.options?.length ?? 0) > 0;
471
471
  const hasEndpoint = !!c.searchEndpoint;
472
- if (!hasStaticOptions && !hasEndpoint && c.type !== 'boolean')
473
- continue;
472
+ // Pick the filter UI from column type:
473
+ // - explicit options or searchEndpoint → multi-select dropdown
474
+ // - boolean → boolean toggle (renders as select under the hood)
475
+ // - number / number_range / numeric → number range
476
+ // - everything else (text, email, phone, tags…) → text contains
477
+ let filterType = 'select';
478
+ if (hasStaticOptions || hasEndpoint)
479
+ filterType = 'select';
480
+ else if (c.type === 'boolean')
481
+ filterType = 'boolean';
482
+ else if (c.type === 'number')
483
+ filterType = 'number_range';
484
+ else
485
+ filterType = 'text';
474
486
  const options = hasStaticOptions
475
487
  ? c.options.map(o => ({
476
488
  label: o.label,
@@ -482,7 +494,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
482
494
  ? filterOptionsMap.get(c.searchEndpoint) || []
483
495
  : [];
484
496
  map.set(c.key, {
485
- filterType: 'select',
497
+ filterType,
486
498
  filterKey: c.key,
487
499
  options,
488
500
  selectedValues: dynamicFilters[c.key] || [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "6.4.0",
3
+ "version": "7.1.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  "date-fns": ">=3",
31
31
  "react-day-picker": ">=8",
32
32
  "@asteby/metacore-sdk": "^2.2.0",
33
- "@asteby/metacore-ui": "^0.6.0"
33
+ "@asteby/metacore-ui": "^0.7.0"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@tanstack/react-router": {
@@ -56,7 +56,7 @@
56
56
  "typescript": "^5.6.0",
57
57
  "zustand": "^5.0.0",
58
58
  "@asteby/metacore-sdk": "2.2.0",
59
- "@asteby/metacore-ui": "0.6.0"
59
+ "@asteby/metacore-ui": "0.7.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc -p tsconfig.json",
@@ -31,7 +31,7 @@ import {
31
31
  FilterableColumnHeader,
32
32
  type ColumnFilterMeta,
33
33
  } from '@asteby/metacore-ui/data-table'
34
- import { generateBadgeStyles } from '@asteby/metacore-ui/lib'
34
+ import { generateBadgeStyles, getInitials } from '@asteby/metacore-ui/lib'
35
35
  import { OptionsContext } from './options-context'
36
36
  import { DynamicIcon } from './dynamic-icon'
37
37
  import type { TableMetadata, ColumnDefinition } from './types'
@@ -329,12 +329,7 @@ export function makeDefaultGetDynamicColumns(
329
329
  className="object-cover"
330
330
  />
331
331
  <AvatarFallback className="text-[10px] font-bold bg-primary/5 text-primary rounded-lg">
332
- {String(name)
333
- .split(' ')
334
- .map((n: string) => n[0])
335
- .slice(0, 2)
336
- .join('')
337
- .toUpperCase()}
332
+ {getInitials(String(name))}
338
333
  </AvatarFallback>
339
334
  </Avatar>
340
335
  <div className="flex flex-col min-w-0 overflow-hidden">
@@ -466,7 +461,42 @@ export function makeDefaultGetDynamicColumns(
466
461
  })
467
462
  })
468
463
 
469
- if (metadata.hasActions && metadata.actions.length > 0) {
464
+ // Resolve which actions to surface in the row dropdown:
465
+ // 1. If the host metadata declares its own actions, use them as-is.
466
+ // 2. Otherwise, when enableCRUDActions is true, fall back to the
467
+ // canonical View / Edit / Delete trio so any model with CRUD on
468
+ // gets the same dropdown without the host having to declare it.
469
+ // The DynamicTable wires `view`/`edit`/`delete` to its own dialogs
470
+ // through onAction, so labels/icons are the only thing this needs to
471
+ // ship.
472
+ const explicitActions = metadata.actions ?? []
473
+ const hasExplicitActions = (metadata.hasActions ?? explicitActions.length > 0) && explicitActions.length > 0
474
+ const defaultCRUDActions: typeof explicitActions =
475
+ metadata.enableCRUDActions
476
+ ? [
477
+ {
478
+ key: 'view',
479
+ name: 'view',
480
+ label: t ? t('datatable.view_record') : 'Ver',
481
+ icon: 'Eye',
482
+ } as any,
483
+ {
484
+ key: 'edit',
485
+ name: 'edit',
486
+ label: t ? t('datatable.edit') : 'Editar',
487
+ icon: 'Pencil',
488
+ } as any,
489
+ {
490
+ key: 'delete',
491
+ name: 'delete',
492
+ label: t ? t('datatable.delete') : 'Eliminar',
493
+ icon: 'Trash2',
494
+ } as any,
495
+ ]
496
+ : []
497
+ const resolvedActions = hasExplicitActions ? explicitActions : defaultCRUDActions
498
+
499
+ if (resolvedActions.length > 0) {
470
500
  columns.push({
471
501
  id: 'actions',
472
502
  header: () => <div className="text-right">{t ? t('common.actions') : 'Acciones'}</div>,
@@ -483,7 +513,7 @@ export function makeDefaultGetDynamicColumns(
483
513
  </Button>
484
514
  </DropdownMenuTrigger>
485
515
  <DropdownMenuContent align="end">
486
- {metadata.actions
516
+ {resolvedActions
487
517
  .filter((action) => {
488
518
  if (!action.condition) return true
489
519
  const { field, operator, value } = action.condition
@@ -156,7 +156,11 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
156
156
  const effectiveHideCreate = hideCreate || ext?.hideCreate
157
157
  const effectiveHideExport = hideExport || ext?.hideExport
158
158
  const effectiveHideImport = hideImport || ext?.hideImport
159
- const effectiveHideRefresh = hideRefresh || ext?.hideRefresh
159
+ // Refresh defaults to hidden in the page header — <DynamicTable> ships
160
+ // its own refresh icon next to the View / column-visibility toolbar, so
161
+ // showing one in the page chrome is just visual duplication. Apps that
162
+ // want it back can pass `hideRefresh={false}`.
163
+ const effectiveHideRefresh = hideRefresh ?? ext?.hideRefresh ?? true
160
164
  const showCreate = enableCRUD && !effectiveHideCreate
161
165
  const showImport = enableCRUD && !effectiveHideImport
162
166
  const showExport = !effectiveHideExport
@@ -507,7 +507,17 @@ export function DynamicTable({
507
507
  if (!c.filterable || map.has(c.key)) continue
508
508
  const hasStaticOptions = (c.options?.length ?? 0) > 0
509
509
  const hasEndpoint = !!c.searchEndpoint
510
- if (!hasStaticOptions && !hasEndpoint && c.type !== 'boolean') continue
510
+ // Pick the filter UI from column type:
511
+ // - explicit options or searchEndpoint → multi-select dropdown
512
+ // - boolean → boolean toggle (renders as select under the hood)
513
+ // - number / number_range / numeric → number range
514
+ // - everything else (text, email, phone, tags…) → text contains
515
+ let filterType: ColumnFilterConfig['filterType'] = 'select'
516
+ if (hasStaticOptions || hasEndpoint) filterType = 'select'
517
+ else if (c.type === 'boolean') filterType = 'boolean'
518
+ else if (c.type === 'number') filterType = 'number_range'
519
+ else filterType = 'text'
520
+
511
521
  const options = hasStaticOptions
512
522
  ? c.options!.map(o => ({
513
523
  label: o.label,
@@ -519,7 +529,7 @@ export function DynamicTable({
519
529
  ? filterOptionsMap.get(c.searchEndpoint!) || []
520
530
  : []
521
531
  map.set(c.key, {
522
- filterType: 'select',
532
+ filterType,
523
533
  filterKey: c.key,
524
534
  options,
525
535
  selectedValues: dynamicFilters[c.key] || [],