From 764a4e5b4ee137713eba48c737b3e0eed5a47e6e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 23 Aug 2026 02:58:07 +0700 Subject: [PATCH] feat(web): enhance action column functionality in data grid components - Introduced `KIT_ACTION_COLUMN_SIZE` for consistent action column sizing across data grids. - Updated `applyKitActionColumn` to enforce action column properties such as fixed size, no sorting, and no resizing. - Enhanced `kitColumnPinning` logic to conditionally enable pinning based on horizontal scrolling. - Added tests for `applyKitActionColumn` and `kitColumnPinning` to ensure expected behavior. - Adjusted `AccessApiKeysGrid`, `ScheduleModulesGrid`, and `ResourcePageFiltered` components to utilize new action column features. --- .../access/access-api-keys-grid.tsx | 1 + .../reui-kit/data-grid-kit-defaults.test.ts | 68 +++++++++++- .../components/reui-kit/frame-data-grid.tsx | 102 +++++++++++++++--- .../src/components/reui-kit/resource-page.tsx | 20 ++-- .../schedule/schedule-modules-grid.tsx | 1 + 5 files changed, 172 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/access/access-api-keys-grid.tsx b/apps/web/src/components/access/access-api-keys-grid.tsx index 38fb94b..b961e3b 100644 --- a/apps/web/src/components/access/access-api-keys-grid.tsx +++ b/apps/web/src/components/access/access-api-keys-grid.tsx @@ -98,6 +98,7 @@ export function AccessApiKeysGrid({ { id: 'actions', enableSorting: false, + size: 88, header: () => null, cell: ({ row }) => { const k = row.original diff --git a/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts b/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts index c47667b..356b356 100644 --- a/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts +++ b/apps/web/src/components/reui-kit/data-grid-kit-defaults.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest' -import { kitDataGridTableClassNames, kitDataGridTableLayout } from './frame-data-grid' +import { + applyKitActionColumn, + KIT_ACTION_COLUMN_SIZE, + kitColumnPinning, + kitDataGridTableClassNames, + kitDataGridTableLayout, +} from './frame-data-grid' describe('kitDataGridTableLayout', () => { it('filtering-2 defaults: dense, headerBackground false, width fixed', () => { @@ -30,3 +36,63 @@ describe('kitDataGridTableLayout', () => { expect(layout.headerBackground).toBe(false) }) }) + +describe('applyKitActionColumn', () => { + it('locks filtering-2 size on id=actions', () => { + const [name, actions] = applyKitActionColumn([ + { id: 'name', header: 'Name' }, + { id: 'actions', header: () => null, cell: () => 'x' }, + ]) + expect(name?.id).toBe('name') + expect(actions?.size).toBe(KIT_ACTION_COLUMN_SIZE) + expect(actions?.minSize).toBe(KIT_ACTION_COLUMN_SIZE) + expect(actions?.maxSize).toBe(KIT_ACTION_COLUMN_SIZE) + expect(actions?.enableSorting).toBe(false) + expect(actions?.enableResizing).toBe(false) + }) + + it('keeps explicit size (data-grid-base-7 text button)', () => { + const [actions] = applyKitActionColumn([ + { id: 'actions', size: 104, cell: () => 'x' }, + ]) + expect(actions?.size).toBe(104) + expect(actions?.minSize).toBe(104) + expect(actions?.maxSize).toBe(104) + }) + + it('does not rewrite a non-actions last column', () => { + const [col] = applyKitActionColumn([{ id: 'name', header: 'Name' }]) + expect(col?.size).toBeUndefined() + expect(col?.enableResizing).toBeUndefined() + }) + + it('applies DNA when pinLastColumn even without id=actions', () => { + const [col] = applyKitActionColumn([{ id: 'other', cell: () => 'x' }], { + pinLastColumn: true, + }) + expect(col?.size).toBe(KIT_ACTION_COLUMN_SIZE) + expect(col?.maxSize).toBe(KIT_ACTION_COLUMN_SIZE) + }) +}) + +describe('kitColumnPinning', () => { + it('does not end-pin without horizontalScroll', () => { + const result = kitColumnPinning({ + pinLastColumn: true, + lastColId: 'actions', + }) + expect(result.enablePinning).toBe(false) + expect(result.columnPinning.end).toEqual([]) + }) + + it('end-pins only with horizontalScroll', () => { + const result = kitColumnPinning({ + pinLastColumn: true, + horizontalScroll: true, + lastColId: 'actions', + }) + expect(result.enablePinning).toBe(true) + expect(result.columnPinning.end).toEqual(['actions']) + }) +}) + diff --git a/apps/web/src/components/reui-kit/frame-data-grid.tsx b/apps/web/src/components/reui-kit/frame-data-grid.tsx index 3fac5e6..762015b 100644 --- a/apps/web/src/components/reui-kit/frame-data-grid.tsx +++ b/apps/web/src/components/reui-kit/frame-data-grid.tsx @@ -87,6 +87,81 @@ export const kitDataGridTableClassNames = { edgeCell: 'first:ps-3 last:pe-3', } as const +/** + * Compact action column size from data-grid-filtering-2. + * Preview: https://reui.io/preview/base/data-grid-filtering-2 + */ +export const KIT_ACTION_COLUMN_SIZE = 56 + +const ACTION_CELL_ALIGN = 'flex items-center justify-end' + +function lastColumnId(columns: DataGridColumnDef[]): string { + const last = columns[columns.length - 1] + if (!last) return '' + if (last.id) return last.id + if ('accessorKey' in last && typeof last.accessorKey === 'string') return last.accessorKey + return '' +} + +function wrapActionCell( + cell: DataGridColumnDef['cell'], +): DataGridColumnDef['cell'] { + if (typeof cell !== 'function') { + return () =>
{cell as ReactNode}
+ } + return (ctx) =>
{cell(ctx)}
+} + +/** + * filtering-2 action column DNA: locked width, no sort/resize, inner justify-end + * (flex on the cell wrapper, never on `td` — that breaks rowBorder alignment). + * Preview: https://reui.io/preview/base/data-grid-filtering-2 + */ +export function applyKitActionColumn( + columns: DataGridColumnDef[], + opts: { pinLastColumn?: boolean } = {}, +): DataGridColumnDef[] { + if (columns.length === 0) return columns + const last = columns[columns.length - 1] + const lastId = lastColumnId(columns) + if (lastId !== 'actions' && !opts.pinLastColumn) return columns + + const size = last.size ?? KIT_ACTION_COLUMN_SIZE + return [ + ...columns.slice(0, -1), + { + ...last, + size, + minSize: last.minSize ?? size, + maxSize: last.maxSize ?? size, + enableSorting: false, + enableResizing: false, + cell: last.cell ? wrapActionCell(last.cell) : last.cell, + }, + ] +} + +/** End-pin only when the grid actually scrolls horizontally (not for action columns). */ +export function kitColumnPinning(opts: { + pinLastColumn?: boolean + horizontalScroll?: boolean + lastColId: string + pinLeftColumnIds?: string[] +}): { + enablePinning: boolean + columnPinning: { start: string[]; end: string[] } +} { + const pinLeft = opts.pinLeftColumnIds ?? [] + const pinEnd = Boolean(opts.pinLastColumn && opts.horizontalScroll && opts.lastColId) + return { + enablePinning: pinEnd || pinLeft.length > 0, + columnPinning: { + start: pinLeft, + end: pinEnd ? [opts.lastColId] : [], + }, + } +} + function loadStoredColumnVisibility(key: string): ColumnVisibilityState | undefined { try { const raw = localStorage.getItem(key) @@ -281,6 +356,7 @@ export function FrameDataGrid({ expandedContent, getRowCanExpand, pinLeftColumnIds, + horizontalScroll = false, tableWidth = 'fixed', columnPinControls = false, }: FrameDataGridProps) { @@ -345,21 +421,23 @@ export function FrameDataGrid({ } const tableColumns: DataGridColumnDef[] = applyColumnPinControls( - [ - ...(expandedContent ? [expandColumn] : []), - ...(enableRowSelection ? [selectColumn] : []), - ...columns, - ], + applyKitActionColumn( + [ + ...(expandedContent ? [expandColumn] : []), + ...(enableRowSelection ? [selectColumn] : []), + ...columns, + ], + { pinLastColumn }, + ), columnPinControls, ) - const lastColId = pinLastColumn ? (tableColumns[tableColumns.length - 1]?.id ?? '') : '' - const pinLeft = pinLeftColumnIds ?? [] - const enablePinning = pinLastColumn || pinLeft.length > 0 - const columnPinning = { - start: pinLeft, - end: pinLastColumn && lastColId ? [lastColId] : [], - } + const { enablePinning, columnPinning } = kitColumnPinning({ + pinLastColumn, + horizontalScroll, + lastColId: lastColumnId(tableColumns), + pinLeftColumnIds, + }) const table = useTable({ features: dataGridFeatures, diff --git a/apps/web/src/components/reui-kit/resource-page.tsx b/apps/web/src/components/reui-kit/resource-page.tsx index 3f71d84..64981b0 100644 --- a/apps/web/src/components/reui-kit/resource-page.tsx +++ b/apps/web/src/components/reui-kit/resource-page.tsx @@ -36,6 +36,8 @@ import { FILTERS_LABELS_RU, FILTERS_OPERATOR_LABELS_RU } from '@/lib/filters-i18 import { applyFiltersToData, createEmptyFilterQuery } from './filter-utils' import { FrameDataGrid, + applyKitActionColumn, + kitColumnPinning, kitDataGridTableClassNames, kitDataGridTableLayout, type DataGridColumnDef, @@ -278,6 +280,7 @@ function ResourcePageFiltered({ onRowClick, virtualization = false, height = 480, + horizontalScroll = false, }: ResourcePageProps) { const headerActions = primaryAction ?? actions const [internalTab, setInternalTab] = useState(tabs?.[0]?.id ?? 'all') @@ -328,12 +331,15 @@ function ResourcePageFiltered({ const selectedCount = selectedIds.length - const lastColId = pinLastColumn ? (columns[columns.length - 1]?.id ?? '') : '' - const enablePinning = Boolean(pinLastColumn && lastColId) - const columnPinning = { - start: [] as string[], - end: enablePinning ? [lastColId] : [], - } + const tableColumns = useMemo( + () => applyKitActionColumn(columns, { pinLastColumn }), + [columns, pinLastColumn], + ) + const { enablePinning, columnPinning } = kitColumnPinning({ + pinLastColumn, + horizontalScroll, + lastColId: tableColumns[tableColumns.length - 1]?.id ?? '', + }) const clearSelection = useCallback(() => { setRowSelection({}) @@ -342,7 +348,7 @@ function ResourcePageFiltered({ const table = useTable({ features: dataGridFeatures, data: filteredData, - columns, + columns: tableColumns, getRowId: (row, index) => getRowId(row, index), state: { sorting, diff --git a/apps/web/src/components/schedule/schedule-modules-grid.tsx b/apps/web/src/components/schedule/schedule-modules-grid.tsx index 0db07e0..5e747af 100644 --- a/apps/web/src/components/schedule/schedule-modules-grid.tsx +++ b/apps/web/src/components/schedule/schedule-modules-grid.tsx @@ -96,6 +96,7 @@ export function ScheduleModulesGrid({ { id: 'actions', enableSorting: false, + size: 104, header: () => null, cell: ({ row }) => (