update-action
Cập nhật action trong cột thao tác của bảng dữ liệu master page KetoanApp. MẶC ĐỊNH áp dụng Pattern B: Edit + Xóa (Button) + Chức năng khác (DropdownMenu: Nhân bản + Ngừng/Kích hoạt). Hỗ trợ 3 pattern
Files1
View on GitHubSKILL.md.claude/skills/_archived-sasuco/update-action/SKILL.md
GitHubUpdate Action — Cập Nhật Cột Thao Tác Bảng Dữ Liệu
Kế thừa:
tao-master-page(cấu trúc page),tao-ui-giao-dien-new(UI foundation),dat-ten(quy tắc data-qa). Áp dụng cho: Master Page trong KetoanApp (src/modules/KetoanApp/features/**/pages/*Page.tsx).
⚠️ MẶC ĐỊNH: Khi user gọi
/update-actioncho 1 page, mặc định áp dụng Pattern B: Sửa (Button) + Xóa (Button) + Chức năng khác (DropdownMenu: Nhân bản, Ngừng/Kích hoạt). Chỉ dùng Pattern A/C khi user yêu cầu rõ ràng.
1. Xác Định Đầu Vào
Trước khi update, xác định rõ:
| Thông tin | Mô tả | Ví dụ |
|---|---|---|
| Page file | Đường dẫn tuyệt đối file Page cần sửa | src/modules/KetoanApp/features/danh-muc/tien-te/pages/TienTePage.tsx |
| Pattern hiện tại | Đang dùng DmRowActions hay Button thủ công? |
useDmRowActions |
| Action đích | Mặc định Pattern B: Sửa + Xóa + DropdownMenu(Nhân bản, Toggle) | — |
Cách xác định pattern hiện tại
Mở page file, tìm trong cột ghost action (DmTableCell sticky right-0):
| Dấu hiệu | Pattern |
|---|---|
Có <Button> + <DropdownMenu> thủ công |
Pattern B — Button thủ công (MẶC ĐỊNH) |
Có <DmRowActions actions={getRowActions(item)} /> |
Pattern A — useDmRowActions |
Có <DmRowActions> nhưng import thêm DropdownMenu |
Pattern C — Hỗn hợp |
2. Ba Pattern Action
Pattern B — Button Sửa + Xóa + DropdownMenu "Chức năng khác" (MẶC ĐỊNH)
Đây là pattern mặc định khi gọi
/update-action. Không cần user chỉ định.
Dùng khi: Cần đầy đủ Sửa / Xóa / Nhân bản / Toggle Active trong 1 layout gọn, chuyên nghiệp.
File tham khảo:
src/modules/KetoanApp/features/danh-muc/tai-khoan-ket-chuyen/pages/CapTaiKhoanPage.tsx
B.1 Imports cần có
import { Button } from '@/shared/components/ui/button'
import { Pencil, Trash2, MoreHorizontal, Copy, Ban, CheckCircle } from 'lucide-react'
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
} from '@/shared/components/ui/dropdown-menu'
⚠️ KHÔNG import
DmRowActionshoặcuseDmRowActions— Pattern B dùng Button thủ công hoàn toàn.
B.2 State & handlers cần có trong component
// State cho dialog CRUD
const [dialogOpen, setDialogOpen] = useState(false)
const [selectedItem, setSelectedItem] = useState<XxxDto | null>(null)
const [cloneFrom, setCloneFrom] = useState<XxxDto | null>(null)
// State cho confirm delete
const [deleteItem, setDeleteItem] = useState<XxxDto | null>(null)
const [deleteOpen, setDeleteOpen] = useState(false)
// Hook dialog form — để lấy handleDelete, handleToggleActive
const { handleDelete, handleToggleActive } = useXxxDialogForm(null)
function handleEdit(item: XxxDto) { setSelectedItem(item); setCloneFrom(null); setDialogOpen(true) }
function handleClone(item: XxxDto) { setCloneFrom(item); setSelectedItem(null); setDialogOpen(true) }
function handleDeleteRow(item: XxxDto) { setDeleteItem(item); setDeleteOpen(true) }
function handleCreate() { setSelectedItem(null); setCloneFrom(null); setDialogOpen(true) }
B.3 Ghost column với Button + DropdownMenu
{/* Cột neo sticky phải — action hiện khi hover row */}
<DmTableCell className='sticky right-0 z-20 bg-transparent'
style={{ width: 0, minWidth: 0, padding: 0, border: 'none', overflow: 'visible' }}>
<div className='absolute right-0 top-0 bottom-0 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-gradient-to-l from-white via-white/90 to-transparent pl-16 pr-2'>
{/* [1] Sửa — luôn có */}
<Button variant='ghost' size='sm'
className='icon-warning border rounded-lg bg-white'
title='Sửa' data-qa={`btn_sua_${item.id}`}
onClick={(e) => { e.stopPropagation(); handleEdit(item) }}>
<Pencil className='h-4 w-4' />
</Button>
{/* [2] Xóa — luôn có */}
<Button variant='ghost' size='sm'
className='icon-danger border rounded-lg bg-white'
title='Xóa' data-qa={`btn_xoa_${item.id}`}
onClick={(e) => { e.stopPropagation(); handleDeleteRow(item) }}>
<Trash2 className='h-4 w-4' />
</Button>
{/* [3] DropdownMenu — Chức năng khác */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='sm'
className='border rounded-lg bg-white'
title='Chức năng khác' data-qa={`btn_khac_${item.id}`}>
<MoreHorizontal className='h-4 w-4' />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-40'>
{/* Nhân bản */}
<DropdownMenuItem onSelect={() => handleClone(item)}
data-qa={`btn_nhan_ban_${item.id}`}>
<Copy className='h-4 w-4' /> Nhân bản
</DropdownMenuItem>
{/* Toggle Active: Ngừng sử dụng / Kích hoạt */}
<DropdownMenuItem
onSelect={async () => {
const ok = await handleToggleActive(item)
if (ok) refetch()
}}
data-qa={`btn_toggle_active_${item.id}`}>
{item.isActive ? (
<><Ban className='h-4 w-4' /> Ngừng sử dụng</>
) : (
<><CheckCircle className='h-4 w-4' /> Kích hoạt</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</DmTableCell>
B.4 Quy tắc icon variant trong Pattern B
| Action | Class | Icon |
|---|---|---|
| Sửa | icon-warning border rounded-lg bg-white |
Pencil |
| Xóa | icon-danger border rounded-lg bg-white |
Trash2 |
| Chức năng khác (trigger) | border rounded-lg bg-white |
MoreHorizontal |
Pattern A — useDmRowActions (chuẩn, CRUD cơ bản)
Dùng khi: Chỉ cần Xem / Sửa / Nhân bản / Xóa — không có action đặc biệt. User phải chỉ định rõ nếu muốn dùng pattern này thay vì mặc định.
// --- IMPORTS ---
import { useDmRowActions } from '@/shared/hooks'
import { DmRowActions } from '@/modules/KetoanApp/components'
// --- HOOK (trong component) ---
const getRowActions = useDmRowActions<XxxDto>({
canView: true, // true = hiện nút Xem (mặc định)
canEdit: true, // true = hiện nút Sửa
canClone: true, // true = hiện nút Nhân bản
canDelete: true, // true = hiện nút Xóa
onView: (item) => { setSelectedItem(item); setDialogOpen(true) },
onEdit: (item) => { setSelectedItem(item); setDialogOpen(true) },
onClone: (item) => { setCloneFrom(item); setSelectedItem(null); setDialogOpen(true) },
onDelete: (item) => { setDeleteItem(item); setDeleteOpen(true) },
})
// --- TRONG GHOST COLUMN ---
<DmTableCell className='sticky right-0 z-20 bg-transparent'
style={{ width: 0, minWidth: 0, padding: 0, border: 'none', overflow: 'visible' }}>
<div className='absolute right-0 top-0 bottom-0 flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-150 bg-gradient-to-l from-white via-white/90 to-transparent pl-12 pr-2'>
<DmRowActions actions={getRowActions(item)} />
</div>
</DmTableCell>
Tắt action: set canXxx: false.
// Ví dụ: không có Nhân bản
const getRowActions = useDmRowActions<XxxDto>({
canClone: false, // ← tắt
onView: ..., onEdit: ..., onDelete: ...,
})
Pattern C — useDmRowActions + extraActions (Toggle Active gọn)
Dùng khi: Vẫn muốn dùng DmRowActions cho các action chuẩn, nhưng cần thêm Toggle Active qua extraActions.
import { useDmRowActions } from '@/shared/hooks'
import { DmRowActions } from '@/modules/KetoanApp/components'
import { Ban, CheckCircle } from 'lucide-react'
const { handleToggleActive } = useXxxDialogForm(null)
const getRowActions = useDmRowActions<XxxDto>({
onView: (item) => { setSelectedItem(item); setDialogOpen(true) },
onEdit: (item) => { setSelectedItem(item); setDialogOpen(true) },
onClone: (item) => { setCloneFrom(item); setSelectedItem(null); setDialogOpen(true) },
onDelete: (item) => { setDeleteItem(item); setDeleteOpen(true) },
extraActions: (item) => [{
icon: item.isActive ? Ban : CheckCircle,
label: item.isActive ? 'Ngừng sử dụng' : 'Kích hoạt',
variant: 'warning',
'data-qa': `btn_toggle_active_${item.id}`,
onClick: async () => { const ok = await handleToggleActive(item); if (ok) refetch() },
}],
})
Ưu điểm: Code gọn, tận dụng
DmRowActionscó sẵn. Nhược điểm: Nếu có >2 action đặc biệt, nên dùng Pattern B (DropdownMenu) để tránh quá nhiều nút.
3. Quy Trình Update Action
Bước 1: Đọc page hiện tại
# Đọc toàn bộ page file để xác định pattern hiện tại
read_file pagePath 1 999
Xác định:
- Đang dùng pattern nào? (A / B / C)
- Có những action nào hiện tại?
- Có state
cloneFromchưa? (cần cho Nhân bản) - Có
handleToggleActivetrong hook dialog form chưa?
Bước 2: Xác định thay đổi cần làm
| Yêu cầu | Pattern đích | Thay đổi |
|---|---|---|
| Thêm Nhân bản | A (bật canClone: true) |
Thêm state cloneFrom, sửa onClone handler |
| Thêm Toggle Active | B hoặc C | Pattern B: thêm DropdownMenu; Pattern C: thêm extraActions |
| Thêm cả Nhân bản + Toggle Active | B | Dùng Button thủ công + DropdownMenu |
| Bỏ Xem (chỉ Sửa/Xóa) | A (tắt canView: false) |
Set canView: false |
| Chuyển từ A sang B | B | Thay DmRowActions → Button + DropdownMenu |
| Chuyển từ B sang A | A | Thay Button + DropdownMenu → DmRowActions |
Bước 3: Kiểm tra dependencies trước khi sửa
Trước khi sửa ghost column, đảm bảo các thứ sau đã có trong page:
| Thành phần | Cần cho action nào | Cách kiểm tra |
|---|---|---|
cloneFrom state |
Nhân bản | Tìm useState<XxxDto | null>(null) thứ 3 |
handleClone() |
Nhân bản | Tìm function handleClone |
handleToggleActive |
Toggle Active | Có trong return của useXxxDialogForm(null) |
deleteItem + deleteOpen state |
Xóa | Tìm useState cho delete |
ConfirmDialog |
Xóa | Tìm <ConfirmDialog cuối JSX |
DropdownMenu import |
Pattern B | Kiểm tra imports từ dropdown-menu |
Nếu thiếu → thêm vào trước khi sửa ghost column.
Bước 4: Sửa ghost column
Thay toàn bộ block <DmTableCell className='sticky right-0...'> bằng pattern mới.
Bước 5: Cập nhật PAGE_FEATURES (nếu có)
export const PAGE_FEATURES = [
{ label: 'Làm mới', code: 'btn-refresh' },
{ label: 'Thêm mới', code: 'btn-create' },
{ label: 'Chỉnh sửa', code: 'row-edit' },
{ label: 'Xóa', code: 'row-delete' },
{ label: 'Nhân bản', code: 'row-clone' }, // ← thêm nếu mới
{ label: 'Ngừng sử dụng / Kích hoạt', code: 'row-toggle-active' }, // ← thêm nếu mới
]
Bước 6: Kiểm tra data-qa
| Action | data-qa pattern |
|---|---|
| Sửa | btn_sua_${item.id} |
| Xóa | btn_xoa_${item.id} |
| Nhân bản | btn_nhan_ban_${item.id} |
| Chức năng khác (trigger) | btn_khac_${item.id} |
| Toggle Active | btn_toggle_active_${item.id} |
4. Các Trường Hợp Cụ Thể
4.1 Thêm Nhân Bản vào page chưa có
Page hiện tại: Dùng useDmRowActions với onClone: () => { setSelectedItem(null); setDialogOpen(true) } (clone rỗng, chưa copy data).
Cần sửa:
- Thêm state:
const [cloneFrom, setCloneFrom] = useState<XxxDto | null>(null) - Sửa
handleCreate:setCloneFrom(null)trước khi mở dialog - Sửa
onClone:onClone: (item) => { setCloneFrom(item); setSelectedItem(null); setDialogOpen(true) } - Thêm prop
cloneFromvào Dialog:<XxxFormDialog ... cloneFrom={cloneFrom} /> - Kiểm tra Dialog đã hỗ trợ
cloneFromprop chưa (xemedit-form-pattern.mdtrong user memory)
4.2 Thêm Toggle Active (Ngừng sử dụng / Kích hoạt)
Điều kiện: DTO phải có field isActive: boolean.
Pattern B — Thêm DropdownMenu:
- Thêm import
DropdownMenu,MoreHorizontal,Ban,CheckCircle,Copy - Đảm bảo hook dialog form export
handleToggleActive - Thay ghost column bằng Pattern B (Section 2.B.3)
- Nếu chưa có
handleDeletetừ hook → destructure thêm - Thêm
ConfirmDialogcho delete nếu chưa có
Pattern C — Thêm extraActions:
- Thêm import
Ban,CheckCircle - Thêm
extraActionsvàouseDmRowActions(Section 2.C) - Đảm bảo hook dialog form export
handleToggleActive
4.3 Bỏ bớt action
| Muốn bỏ | Cách làm |
|---|---|
| Xem | Set canView: false (Pattern A) hoặc xóa Button Xem (Pattern B) |
| Nhân bản | Set canClone: false (Pattern A) hoặc xóa DropdownMenuItem (Pattern B) |
| Xóa | Set canDelete: false (Pattern A) hoặc xóa Button Xóa (Pattern B) |
| Toggle Active | Xóa DropdownMenuItem (Pattern B) hoặc xóa extraActions (Pattern C) |
4.4 Chuyển từ Pattern A → Pattern B
Khi cần thêm Toggle Active hoặc action tùy chỉnh không có trong useDmRowActions:
- Xóa import
DmRowActions,useDmRowActions - Thêm import
DropdownMenu,MoreHorizontal,Ban,CheckCircle,Copy,Pencil,Trash2 - Thay
getRowActions+<DmRowActions>bằng Button thủ công + DropdownMenu (Section 2.B.3) - Đảm bảo có
handleEdit,handleClone,handleDeleteRow,handleToggleActive
5. Checklist Sau Khi Update
- Tất cả action có
data-qađúng format (btn_{action}_{item.id}) - Icon đúng variant class (
icon-warningcho Sửa,icon-dangercho Xóa) - Button trong ghost column có
border rounded-lg bg-white -
cloneFromstate được reset tronghandleCreate() - Dialog có prop
cloneFrom={cloneFrom}(nếu có Nhân bản) -
handleToggleActivegọirefetch()sau khi thành công -
ConfirmDialogcho delete có đủopen,onOpenChange,onConfirm -
PAGE_FEATURESđược cập nhật nếu thêm action mới - Không import thừa (nếu bỏ
DmRowActionsthì xóa import) - Code compile không lỗi
6. Ví Dụ Đầy Đủ
Ví dụ 1: Page đang dùng Pattern A, thêm Toggle Active bằng Pattern C
Trước:
const getRowActions = useDmRowActions<CurrencyDto>({
onView: (item) => { setSelectedItem(item); setDialogOpen(true) },
onEdit: (item) => { setSelectedItem(item); setDialogOpen(true) },
onClone: () => { setSelectedItem(null); setDialogOpen(true) },
onDelete: (item) => { setDeleteItem(item); setDeleteOpen(true) },
})
Sau:
import { Ban, CheckCircle } from 'lucide-react'
const { handleDelete, handleBulkDelete, handleToggleActive } = useTTDialogForm(null)
const getRowActions = useDmRowActions<CurrencyDto>({
onView: (item) => { setSelectedItem(item); setDialogOpen(true) },
onEdit: (item) => { setSelectedItem(item); setDialogOpen(true) },
onClone: (item) => { setCloneFrom(item); setSelectedItem(null); setDialogOpen(true) },
onDelete: (item) => { setDeleteItem(item); setDeleteOpen(true) },
extraActions: (item) => [{
icon: item.isActive ? Ban : CheckCircle,
label: item.isActive ? 'Ngừng sử dụng' : 'Kích hoạt',
variant: 'warning',
'data-qa': `btn_toggle_active_${item.id}`,
onClick: async () => { const ok = await handleToggleActive(item); if (ok) refetch() },
}],
})
Ví dụ 2: Page chưa có action nào, tạo mới toàn bộ bằng Pattern B
Tham khảo toàn bộ file: src/modules/KetoanApp/features/danh-muc/tai-khoan-ket-chuyen/pages/CapTaiKhoanPage.tsx
Các phần cần có:
- Imports:
Pencil,Trash2,MoreHorizontal,Copy,Ban,CheckCircle,DropdownMenu* - State:
dialogOpen,selectedItem,cloneFrom,deleteItem,deleteOpen - Handlers:
handleEdit,handleClone,handleDeleteRow,handleCreate - Hook:
const { handleDelete, handleBulkDelete, handleToggleActive } = useXxxDialogForm(null) - Ghost column: Button Sửa + Button Xóa + DropdownMenu (Nhân bản + Toggle Active)
ConfirmDialogcho delete