@asteby/metacore-runtime-react 20.1.6 → 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/dynamic-columns.d.ts +12 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +31 -20
- package/dist/dynamic-kanban.d.ts +13 -1
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +66 -29
- package/dist/dynamic-row-actions.d.ts +36 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -0
- package/dist/dynamic-row-actions.js +110 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +11 -76
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/permissions-context.d.ts +15 -1
- package/dist/permissions-context.d.ts.map +1 -1
- package/dist/permissions-context.js +29 -0
- package/dist/use-dynamic-filters.d.ts +38 -0
- package/dist/use-dynamic-filters.d.ts.map +1 -0
- package/dist/use-dynamic-filters.js +226 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +67 -6
- package/src/__tests__/resolve-row-actions.test.ts +80 -0
- package/src/dynamic-columns.tsx +33 -19
- package/src/dynamic-kanban.tsx +147 -47
- package/src/dynamic-row-actions.tsx +195 -0
- package/src/dynamic-table.tsx +12 -107
- package/src/index.ts +11 -0
- package/src/permissions-context.tsx +39 -0
- package/src/use-dynamic-filters.ts +272 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// useDynamicRowActions — the single source of truth for what a per-row action
|
|
2
|
+
// (the "…" kebab → Ver / Editar / Eliminar / custom / link) DOES when clicked.
|
|
3
|
+
//
|
|
4
|
+
// Extracted verbatim from DynamicTable so the kanban board's per-card menu
|
|
5
|
+
// behaves IDENTICALLY to a table row (it used to forward the raw action object
|
|
6
|
+
// to the host and silently no-op). Both renderers call the returned
|
|
7
|
+
// `handleInternalAction(actionKey, row)` with the action's STRING key and mount
|
|
8
|
+
// the returned `dialogs` node once in their tree:
|
|
9
|
+
//
|
|
10
|
+
// delete → opens the SDK's own confirm dialog, then DELETEs.
|
|
11
|
+
// view / edit → host `onAction(action, row)` when provided (string
|
|
12
|
+
// contract), else the built-in record dialog.
|
|
13
|
+
// link action → navigate to the action's templated `linkUrl`.
|
|
14
|
+
// custom (fields/confirm/
|
|
15
|
+
// executable) action → opens the ActionModal via the dispatcher.
|
|
16
|
+
// anything else → host `onAction` + refresh (or just refresh).
|
|
17
|
+
//
|
|
18
|
+
// The host `onAction` contract is a STRING action key on purpose — the existing
|
|
19
|
+
// ops `handleAction(action: string, row)` keeps working unchanged.
|
|
20
|
+
import { useCallback, useState } from 'react'
|
|
21
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
22
|
+
import { useTranslation } from 'react-i18next'
|
|
23
|
+
import { toast } from 'sonner'
|
|
24
|
+
import {
|
|
25
|
+
AlertDialog,
|
|
26
|
+
AlertDialogAction,
|
|
27
|
+
AlertDialogCancel,
|
|
28
|
+
AlertDialogContent,
|
|
29
|
+
AlertDialogDescription,
|
|
30
|
+
AlertDialogFooter,
|
|
31
|
+
AlertDialogHeader,
|
|
32
|
+
AlertDialogTitle,
|
|
33
|
+
} from '@asteby/metacore-ui/primitives'
|
|
34
|
+
import { useApi } from './api-context'
|
|
35
|
+
import { ActionModalDispatcher } from './action-modal-dispatcher'
|
|
36
|
+
import { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
37
|
+
import type { TableMetadata, ActionMetadata } from './types'
|
|
38
|
+
|
|
39
|
+
export interface UseDynamicRowActionsParams {
|
|
40
|
+
/** Model key as registered on the backend (e.g. "issue"). */
|
|
41
|
+
model: string
|
|
42
|
+
/**
|
|
43
|
+
* Data endpoint base. The per-record DELETE hits `<endpoint>/<id>`; when
|
|
44
|
+
* omitted it falls back to `/data/<model>/<id>`. Pass the SAME base the
|
|
45
|
+
* caller lists from (e.g. `/data/<model>/me`) so writes stay org-scoped.
|
|
46
|
+
*/
|
|
47
|
+
endpoint?: string
|
|
48
|
+
/** Raw model metadata — used to resolve link/custom action definitions. */
|
|
49
|
+
metadata: TableMetadata | null
|
|
50
|
+
/**
|
|
51
|
+
* Host hook for `view`/`edit` (and any unhandled key). STRING contract:
|
|
52
|
+
* receives the action key, not the action object. When absent, `view`/`edit`
|
|
53
|
+
* fall back to the built-in record dialog.
|
|
54
|
+
*/
|
|
55
|
+
onAction?: (action: string, row: any) => void
|
|
56
|
+
/** Called after a successful delete / custom action to refetch the list. */
|
|
57
|
+
onRefresh: () => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DynamicRowActions {
|
|
61
|
+
/** Dispatch a row action by its STRING key. */
|
|
62
|
+
handleInternalAction: (action: string, row: any) => Promise<void>
|
|
63
|
+
/**
|
|
64
|
+
* The delete-confirm dialog + record dialog + action modal this handler
|
|
65
|
+
* drives. Render it ONCE in the consuming component's tree.
|
|
66
|
+
*/
|
|
67
|
+
dialogs: React.ReactElement
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Owns the state + dialogs + dispatch logic for per-row actions, shared by
|
|
72
|
+
* DynamicTable and DynamicKanban so both behave identically.
|
|
73
|
+
*/
|
|
74
|
+
export function useDynamicRowActions({
|
|
75
|
+
model,
|
|
76
|
+
endpoint,
|
|
77
|
+
metadata,
|
|
78
|
+
onAction,
|
|
79
|
+
onRefresh,
|
|
80
|
+
}: UseDynamicRowActionsParams): DynamicRowActions {
|
|
81
|
+
const { t } = useTranslation()
|
|
82
|
+
const api = useApi()
|
|
83
|
+
const navigate = useNavigate()
|
|
84
|
+
|
|
85
|
+
const [recordDialog, setRecordDialog] = useState<{
|
|
86
|
+
open: boolean
|
|
87
|
+
mode: 'view' | 'edit' | 'create'
|
|
88
|
+
recordId: string | null
|
|
89
|
+
}>({ open: false, mode: 'view', recordId: null })
|
|
90
|
+
|
|
91
|
+
const [rowToDelete, setRowToDelete] = useState<any | null>(null)
|
|
92
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
93
|
+
|
|
94
|
+
const [actionModal, setActionModal] = useState<{
|
|
95
|
+
open: boolean
|
|
96
|
+
action: ActionMetadata | null
|
|
97
|
+
record: any | null
|
|
98
|
+
}>({ open: false, action: null, record: null })
|
|
99
|
+
|
|
100
|
+
const handleInternalAction = useCallback(async (action: string, row: any) => {
|
|
101
|
+
if (action === 'delete') { setRowToDelete(row); return }
|
|
102
|
+
if (action === 'view' || action === 'edit') {
|
|
103
|
+
if (onAction) await Promise.resolve(onAction(action, row))
|
|
104
|
+
else setRecordDialog({ open: true, mode: action, recordId: row.id })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const linkDef = metadata?.actions?.find((a) => a.key === action && a.type === 'link')
|
|
108
|
+
if (linkDef?.linkUrl) {
|
|
109
|
+
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) => String(row[field] ?? ''))
|
|
110
|
+
navigate({ to: url })
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
const actionDef = metadata?.actions?.find((a) => a.key === action)
|
|
114
|
+
if (actionDef && (actionDef.fields?.length || actionDef.confirm || actionDef.executable)) {
|
|
115
|
+
setActionModal({
|
|
116
|
+
open: true,
|
|
117
|
+
action: {
|
|
118
|
+
key: actionDef.key,
|
|
119
|
+
label: actionDef.label,
|
|
120
|
+
icon: actionDef.icon || 'Zap',
|
|
121
|
+
color: actionDef.color,
|
|
122
|
+
confirm: actionDef.confirm,
|
|
123
|
+
confirmMessage: actionDef.confirmMessage,
|
|
124
|
+
fields: actionDef.fields,
|
|
125
|
+
requiresState: actionDef.requiresState,
|
|
126
|
+
executable: actionDef.executable,
|
|
127
|
+
},
|
|
128
|
+
record: row,
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (onAction) { await Promise.resolve(onAction(action, row)); onRefresh() }
|
|
133
|
+
else onRefresh()
|
|
134
|
+
}, [onAction, onRefresh, metadata, navigate])
|
|
135
|
+
|
|
136
|
+
const confirmDelete = async () => {
|
|
137
|
+
if (!rowToDelete) return
|
|
138
|
+
setIsDeleting(true)
|
|
139
|
+
try {
|
|
140
|
+
const deleteEndpoint = endpoint ? `${endpoint}/${rowToDelete.id}` : `/data/${model}/${rowToDelete.id}`
|
|
141
|
+
const res = await api.delete(deleteEndpoint)
|
|
142
|
+
if (res.data.success) { toast.success(res.data.message || 'Eliminado correctamente'); onRefresh() }
|
|
143
|
+
else toast.error(res.data.message || 'Error al eliminar')
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error al eliminar', error)
|
|
146
|
+
toast.error('Error al eliminar el registro')
|
|
147
|
+
} finally {
|
|
148
|
+
setIsDeleting(false)
|
|
149
|
+
setRowToDelete(null)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dialogs = (
|
|
154
|
+
<>
|
|
155
|
+
<AlertDialog open={!!rowToDelete} onOpenChange={(open: boolean) => !open && setRowToDelete(null)}>
|
|
156
|
+
<AlertDialogContent>
|
|
157
|
+
<AlertDialogHeader>
|
|
158
|
+
<AlertDialogTitle>¿Está absolutamente seguro?</AlertDialogTitle>
|
|
159
|
+
<AlertDialogDescription>Esta acción no se puede deshacer. Esto eliminará permanentemente el registro seleccionado de nuestros servidores.</AlertDialogDescription>
|
|
160
|
+
</AlertDialogHeader>
|
|
161
|
+
<AlertDialogFooter>
|
|
162
|
+
<AlertDialogCancel disabled={isDeleting}>{t('common.cancel')}</AlertDialogCancel>
|
|
163
|
+
<AlertDialogAction onClick={(e: React.MouseEvent) => { e.preventDefault(); confirmDelete() }} className="bg-red-600 hover:bg-red-700" disabled={isDeleting}>
|
|
164
|
+
{isDeleting ? 'Eliminando...' : 'Eliminar'}
|
|
165
|
+
</AlertDialogAction>
|
|
166
|
+
</AlertDialogFooter>
|
|
167
|
+
</AlertDialogContent>
|
|
168
|
+
</AlertDialog>
|
|
169
|
+
|
|
170
|
+
<DynamicRecordDialog
|
|
171
|
+
open={recordDialog.open}
|
|
172
|
+
onOpenChange={(open: boolean) => setRecordDialog((prev) => ({ ...prev, open }))}
|
|
173
|
+
mode={recordDialog.mode}
|
|
174
|
+
model={model}
|
|
175
|
+
recordId={recordDialog.recordId}
|
|
176
|
+
endpoint={endpoint}
|
|
177
|
+
onSaved={onRefresh}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
{actionModal.action && (
|
|
181
|
+
<ActionModalDispatcher
|
|
182
|
+
open={actionModal.open}
|
|
183
|
+
onOpenChange={(open: boolean) => setActionModal((prev) => ({ ...prev, open }))}
|
|
184
|
+
action={actionModal.action}
|
|
185
|
+
model={model}
|
|
186
|
+
record={actionModal.record}
|
|
187
|
+
endpoint={endpoint}
|
|
188
|
+
onSuccess={onRefresh}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return { handleInternalAction, dialogs }
|
|
195
|
+
}
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
// `getDynamicColumns` prop (hosts retain ownership because the rendered
|
|
12
12
|
// column cells are tightly coupled to their design system).
|
|
13
13
|
import { useEffect, useState, useMemo, useCallback, useRef } from 'react'
|
|
14
|
-
import { useNavigate } from '@tanstack/react-router'
|
|
15
14
|
import { useTranslation } from 'react-i18next'
|
|
16
15
|
import { format } from 'date-fns'
|
|
17
16
|
import type { DateRange } from 'react-day-picker'
|
|
@@ -68,11 +67,10 @@ import { useApi, useCurrentBranch } from './api-context'
|
|
|
68
67
|
import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
|
|
69
68
|
import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns'
|
|
70
69
|
import { OptionsContext } from './options-context'
|
|
71
|
-
import {
|
|
72
|
-
import type { TableMetadata, ApiResponse, ActionMetadata } from './types'
|
|
70
|
+
import type { TableMetadata, ApiResponse } from './types'
|
|
73
71
|
import { getSearchableColumnKeys } from './column-visibility'
|
|
74
72
|
import { useCan, usePermissionsActive, gateTableMetadata } from './permissions-context'
|
|
75
|
-
import {
|
|
73
|
+
import { useDynamicRowActions } from './dynamic-row-actions'
|
|
76
74
|
import { ExportDialog } from './dialogs/export'
|
|
77
75
|
import { ImportDialog } from './dialogs/import'
|
|
78
76
|
|
|
@@ -131,7 +129,6 @@ export function DynamicTable({
|
|
|
131
129
|
const { t, i18n } = useTranslation()
|
|
132
130
|
const api = useApi()
|
|
133
131
|
const currentBranch = useCurrentBranch()
|
|
134
|
-
const navigate = useNavigate()
|
|
135
132
|
|
|
136
133
|
const prevBranchId = useRef(currentBranch?.id)
|
|
137
134
|
|
|
@@ -147,24 +144,9 @@ export function DynamicTable({
|
|
|
147
144
|
const [loadingData, setLoadingData] = useState(true)
|
|
148
145
|
const [optionsMap, setOptionsMap] = useState<Map<string, any[]>>(new Map())
|
|
149
146
|
|
|
150
|
-
const [recordDialog, setRecordDialog] = useState<{
|
|
151
|
-
open: boolean
|
|
152
|
-
mode: 'view' | 'edit' | 'create'
|
|
153
|
-
recordId: string | null
|
|
154
|
-
}>({ open: false, mode: 'view', recordId: null })
|
|
155
|
-
|
|
156
|
-
const [rowToDelete, setRowToDelete] = useState<any | null>(null)
|
|
157
|
-
const [isDeleting, setIsDeleting] = useState(false)
|
|
158
|
-
|
|
159
147
|
const [exportOpen, setExportOpen] = useState(false)
|
|
160
148
|
const [importOpen, setImportOpen] = useState(false)
|
|
161
149
|
|
|
162
|
-
const [actionModal, setActionModal] = useState<{
|
|
163
|
-
open: boolean
|
|
164
|
-
action: ActionMetadata | null
|
|
165
|
-
record: any | null
|
|
166
|
-
}>({ open: false, action: null, record: null })
|
|
167
|
-
|
|
168
150
|
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false)
|
|
169
151
|
const [isBulkDeleting, setIsBulkDeleting] = useState(false)
|
|
170
152
|
const [bulkDeleteProgress, setBulkDeleteProgress] = useState(0)
|
|
@@ -506,58 +488,15 @@ export function DynamicTable({
|
|
|
506
488
|
|
|
507
489
|
const handleRefresh = useCallback(() => { fetchData(); fetchAggregates() }, [fetchData, fetchAggregates])
|
|
508
490
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) => String(row[field] ?? ''))
|
|
519
|
-
navigate({ to: url })
|
|
520
|
-
return
|
|
521
|
-
}
|
|
522
|
-
const actionDef = metadata?.actions?.find((a) => a.key === action)
|
|
523
|
-
if (actionDef && (actionDef.fields?.length || actionDef.confirm || actionDef.executable)) {
|
|
524
|
-
setActionModal({
|
|
525
|
-
open: true,
|
|
526
|
-
action: {
|
|
527
|
-
key: actionDef.key,
|
|
528
|
-
label: actionDef.label,
|
|
529
|
-
icon: actionDef.icon || 'Zap',
|
|
530
|
-
color: actionDef.color,
|
|
531
|
-
confirm: actionDef.confirm,
|
|
532
|
-
confirmMessage: actionDef.confirmMessage,
|
|
533
|
-
fields: actionDef.fields,
|
|
534
|
-
requiresState: actionDef.requiresState,
|
|
535
|
-
executable: actionDef.executable,
|
|
536
|
-
},
|
|
537
|
-
record: row,
|
|
538
|
-
})
|
|
539
|
-
return
|
|
540
|
-
}
|
|
541
|
-
if (onAction) { await Promise.resolve(onAction(action, row)); handleRefresh() }
|
|
542
|
-
else handleRefresh()
|
|
543
|
-
}, [onAction, handleRefresh, metadata, navigate])
|
|
544
|
-
|
|
545
|
-
const confirmDelete = async () => {
|
|
546
|
-
if (!rowToDelete) return
|
|
547
|
-
setIsDeleting(true)
|
|
548
|
-
try {
|
|
549
|
-
const deleteEndpoint = endpoint ? `${endpoint}/${rowToDelete.id}` : `/data/${model}/${rowToDelete.id}`
|
|
550
|
-
const res = await api.delete(deleteEndpoint)
|
|
551
|
-
if (res.data.success) { toast.success(res.data.message || 'Eliminado correctamente'); handleRefresh() }
|
|
552
|
-
else toast.error(res.data.message || 'Error al eliminar')
|
|
553
|
-
} catch (error) {
|
|
554
|
-
console.error('Error al eliminar', error)
|
|
555
|
-
toast.error('Error al eliminar el registro')
|
|
556
|
-
} finally {
|
|
557
|
-
setIsDeleting(false)
|
|
558
|
-
setRowToDelete(null)
|
|
559
|
-
}
|
|
560
|
-
}
|
|
491
|
+
// Per-row action dispatch (view/edit/delete/link/custom) + its dialogs live
|
|
492
|
+
// in the shared hook so DynamicKanban's card menu behaves identically.
|
|
493
|
+
const { handleInternalAction, dialogs: rowActionDialogs } = useDynamicRowActions({
|
|
494
|
+
model,
|
|
495
|
+
endpoint,
|
|
496
|
+
metadata,
|
|
497
|
+
onAction,
|
|
498
|
+
onRefresh: handleRefresh,
|
|
499
|
+
})
|
|
561
500
|
|
|
562
501
|
const confirmBulkDelete = async () => {
|
|
563
502
|
const selectedRows = table.getFilteredSelectedRowModel().rows
|
|
@@ -974,20 +913,7 @@ export function DynamicTable({
|
|
|
974
913
|
</div>
|
|
975
914
|
</div>
|
|
976
915
|
|
|
977
|
-
|
|
978
|
-
<AlertDialogContent>
|
|
979
|
-
<AlertDialogHeader>
|
|
980
|
-
<AlertDialogTitle>¿Está absolutamente seguro?</AlertDialogTitle>
|
|
981
|
-
<AlertDialogDescription>Esta acción no se puede deshacer. Esto eliminará permanentemente el registro seleccionado de nuestros servidores.</AlertDialogDescription>
|
|
982
|
-
</AlertDialogHeader>
|
|
983
|
-
<AlertDialogFooter>
|
|
984
|
-
<AlertDialogCancel disabled={isDeleting}>{t('common.cancel')}</AlertDialogCancel>
|
|
985
|
-
<AlertDialogAction onClick={(e: React.MouseEvent) => { e.preventDefault(); confirmDelete() }} className="bg-red-600 hover:bg-red-700" disabled={isDeleting}>
|
|
986
|
-
{isDeleting ? 'Eliminando...' : 'Eliminar'}
|
|
987
|
-
</AlertDialogAction>
|
|
988
|
-
</AlertDialogFooter>
|
|
989
|
-
</AlertDialogContent>
|
|
990
|
-
</AlertDialog>
|
|
916
|
+
{rowActionDialogs}
|
|
991
917
|
|
|
992
918
|
<AlertDialog open={showBulkDeleteConfirm} onOpenChange={(open: boolean) => !open && !isBulkDeleting && setShowBulkDeleteConfirm(false)}>
|
|
993
919
|
<AlertDialogContent>
|
|
@@ -1013,33 +939,12 @@ export function DynamicTable({
|
|
|
1013
939
|
</AlertDialogContent>
|
|
1014
940
|
</AlertDialog>
|
|
1015
941
|
|
|
1016
|
-
<DynamicRecordDialog
|
|
1017
|
-
open={recordDialog.open}
|
|
1018
|
-
onOpenChange={(open: boolean) => setRecordDialog((prev) => ({ ...prev, open }))}
|
|
1019
|
-
mode={recordDialog.mode}
|
|
1020
|
-
model={model}
|
|
1021
|
-
recordId={recordDialog.recordId}
|
|
1022
|
-
endpoint={endpoint}
|
|
1023
|
-
onSaved={handleRefresh}
|
|
1024
|
-
/>
|
|
1025
|
-
|
|
1026
942
|
{viewMetadata?.canExport && (
|
|
1027
943
|
<ExportDialog open={exportOpen} onOpenChange={setExportOpen} model={model} metadata={metadata} currentFilters={buildFilterParams()} hasActiveFilters={hasActiveFilters} />
|
|
1028
944
|
)}
|
|
1029
945
|
{viewMetadata?.canImport && (
|
|
1030
946
|
<ImportDialog open={importOpen} onOpenChange={setImportOpen} model={model} metadata={metadata} onImported={handleRefresh} />
|
|
1031
947
|
)}
|
|
1032
|
-
{actionModal.action && (
|
|
1033
|
-
<ActionModalDispatcher
|
|
1034
|
-
open={actionModal.open}
|
|
1035
|
-
onOpenChange={(open: boolean) => setActionModal((prev) => ({ ...prev, open }))}
|
|
1036
|
-
action={actionModal.action}
|
|
1037
|
-
model={model}
|
|
1038
|
-
record={actionModal.record}
|
|
1039
|
-
endpoint={endpoint}
|
|
1040
|
-
onSuccess={handleRefresh}
|
|
1041
|
-
/>
|
|
1042
|
-
)}
|
|
1043
948
|
<DataTableBulkActions table={table} entityName="registro">
|
|
1044
949
|
<Button variant="destructive" size="sm" className="h-8" onClick={() => setShowBulkDeleteConfirm(true)}>
|
|
1045
950
|
<Trash2 className="h-4 w-4 mr-1.5" /> Eliminar
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,11 @@ export {
|
|
|
23
23
|
resolveActiveView,
|
|
24
24
|
type DynamicViewProps,
|
|
25
25
|
} from './dynamic-view'
|
|
26
|
+
export {
|
|
27
|
+
useDynamicFilters,
|
|
28
|
+
type UseDynamicFiltersOptions,
|
|
29
|
+
type UseDynamicFiltersResult,
|
|
30
|
+
} from './use-dynamic-filters'
|
|
26
31
|
export * from './dynamic-form'
|
|
27
32
|
export {
|
|
28
33
|
ActionModalDispatcher,
|
|
@@ -53,9 +58,15 @@ export {
|
|
|
53
58
|
capabilityForActionKey,
|
|
54
59
|
modelCapability,
|
|
55
60
|
gateTableMetadata,
|
|
61
|
+
resolveRowActions,
|
|
56
62
|
type CanFn,
|
|
57
63
|
type PermissionsProviderProps,
|
|
58
64
|
} from './permissions-context'
|
|
65
|
+
export {
|
|
66
|
+
useDynamicRowActions,
|
|
67
|
+
type UseDynamicRowActionsParams,
|
|
68
|
+
type DynamicRowActions,
|
|
69
|
+
} from './dynamic-row-actions'
|
|
59
70
|
export {
|
|
60
71
|
PermissionsManager,
|
|
61
72
|
moduleActionCapability,
|
|
@@ -156,3 +156,42 @@ export function gateTableMetadata(
|
|
|
156
156
|
metadata.canCreate === undefined ? undefined : metadata.canCreate && allowed('create'),
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolves the per-row (`placement: 'row'`) action list for a model exactly the
|
|
162
|
+
* way DynamicTable's action column does, so the kanban card menu shows the SAME
|
|
163
|
+
* actions:
|
|
164
|
+
* - when a <PermissionsProvider> is active, the metadata is run through
|
|
165
|
+
* `gateTableMetadata` first (filters by capability + materializes the CRUD
|
|
166
|
+
* trio), otherwise the raw metadata is used.
|
|
167
|
+
* - explicit actions win; absent them, the implicit View/Edit/Delete trio is
|
|
168
|
+
* materialized when `enableCRUDActions` is on.
|
|
169
|
+
* - table/create-placement actions are stripped (they belong to the toolbar).
|
|
170
|
+
*
|
|
171
|
+
* Pure. `tx` resolves the trio's i18n labels (defaults to the Spanish fallbacks).
|
|
172
|
+
*/
|
|
173
|
+
export function resolveRowActions(
|
|
174
|
+
metadata: TableMetadata,
|
|
175
|
+
model: string,
|
|
176
|
+
can: CanFn,
|
|
177
|
+
permissionsActive: boolean,
|
|
178
|
+
tx: (i18nKey: string, fallback: string) => string = (_k, fallback) => fallback,
|
|
179
|
+
): ActionDefinition[] {
|
|
180
|
+
const gated = permissionsActive ? gateTableMetadata(metadata, model, can, tx) : metadata
|
|
181
|
+
const explicit = gated.actions ?? []
|
|
182
|
+
const hasExplicit = (gated.hasActions ?? explicit.length > 0) && explicit.length > 0
|
|
183
|
+
const base: ActionDefinition[] = hasExplicit
|
|
184
|
+
? explicit
|
|
185
|
+
: gated.enableCRUDActions
|
|
186
|
+
? DEFAULT_TRIO.map(
|
|
187
|
+
(a) =>
|
|
188
|
+
({
|
|
189
|
+
key: a.key,
|
|
190
|
+
name: a.key,
|
|
191
|
+
label: tx(a.i18nKey, a.fallback),
|
|
192
|
+
icon: a.icon,
|
|
193
|
+
}) as ActionDefinition,
|
|
194
|
+
)
|
|
195
|
+
: []
|
|
196
|
+
return base.filter((a) => ((a.placement ?? 'row') as string) === 'row')
|
|
197
|
+
}
|