feat(design-preview): перетаскивание строк в «Ссылках приложений» через dnd-kit
quality / changes (push) Successful in 9s
quality / commitlint (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m32s
quality / api (push) Successful in 42s
CD / quality (push) Successful in 2m25s
CD / publish (push) Successful in 2m52s

PreviewSectionGrid получил onReorder: ручка DataGridTableDndRowHandle и DataGridTableDndRows
из ReUI. Порядок меняется перетаскиванием, меню строки осталось запасным путём.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-18 02:03:05 +07:00
1 parent 4f5026980a
commit 41f5669812
2 files changed
+50 -9

No files matched your search

@@ -4,7 +4,6 @@ import {
CloudIcon, CloudIcon,
EyeIcon, EyeIcon,
FingerprintIcon, FingerprintIcon,
GripVerticalIcon,
KeyRoundIcon, KeyRoundIcon,
LayoutDashboardIcon, LayoutDashboardIcon,
LayoutGridIcon, LayoutGridIcon,
@@ -141,6 +140,15 @@ export function AppLinksPage() {
[addChange], [addChange],
) )
/** Перетаскивание: строка встаёт на место той, на которую её бросили. */
const dropOn = useCallback(
(activeId: string, overId: string) => {
const to = rowsRef.current.findIndex((r) => r.id === overId)
if (to >= 0) moveTo(activeId as AppId, () => to)
},
[moveTo],
)
const resetOrder = useCallback(() => { const resetOrder = useCallback(() => {
const before = rowsRef.current const before = rowsRef.current
if (before.every((r, i) => r.id === INITIAL[i]?.id)) return if (before.every((r, i) => r.id === INITIAL[i]?.id)) return
@@ -158,10 +166,9 @@ export function AppLinksPage() {
{ {
id: 'order', id: 'order',
title: '№', title: '№',
size: 76, size: 52,
cell: (r) => ( cell: (r) => (
<span className="text-muted-foreground flex items-center gap-1 text-xs tabular-nums"> <span className="text-muted-foreground text-sm tabular-nums">
<GripVerticalIcon className="size-4" aria-hidden />
{rows.findIndex((x) => x.id === r.id) + 1} {rows.findIndex((x) => x.id === r.id) + 1}
</span> </span>
), ),
@@ -267,7 +274,7 @@ export function AppLinksPage() {
<TabsContent value="list" className="min-w-0"> <TabsContent value="list" className="min-w-0">
<PreviewSectionGrid <PreviewSectionGrid
title="Ссылки сервисов" title="Ссылки сервисов"
description="Порядок строк = порядок в меню · изменения применяются после сохранения" description="Перетащите строку за ручку: порядок строк = порядок в меню · изменения применяются после сохранения"
rows={rows} rows={rows}
columns={columns} columns={columns}
getRowId={(r) => r.id} getRowId={(r) => r.id}
@@ -296,6 +303,7 @@ export function AppLinksPage() {
</Button> </Button>
</> </>
)} )}
onReorder={dropOn}
onRowClick={setEditing} onRowClick={setEditing}
/> />
</TabsContent> </TabsContent>
+37 -4
View File
@@ -6,6 +6,11 @@ import { DataGrid, dataGridFeatures, type DataGridFeatures } from '@/components/
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination' import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area' import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
import {
DataGridTableDndRowHandle,
DataGridTableDndRows,
} from '@/components/reui/data-grid/data-grid-table-dnd-rows'
import type { DragEndEvent } from '@dnd-kit/core'
import { import {
DataGridTable, DataGridTable,
DataGridTableRowSelect, DataGridTableRowSelect,
@@ -40,6 +45,7 @@ export type GridColumn<T> = {
export type GridView<T> = { label: string; match: (row: T) => boolean } export type GridView<T> = { label: string; match: (row: T) => boolean }
const SELECT_COLUMN_SIZE = 44 const SELECT_COLUMN_SIZE = 44
const DRAG_COLUMN_SIZE = 44
/** /**
* Секция-таблица макета: Frame + Filters-подобная панель + DataGrid на DataGridScrollArea. * Секция-таблица макета: Frame + Filters-подобная панель + DataGrid на DataGridScrollArea.
@@ -59,6 +65,7 @@ export function PreviewSectionGrid<T extends object>({
selectable = false, selectable = false,
bulkActions, bulkActions,
toolbar, toolbar,
onReorder,
pageSize = 8, pageSize = 8,
emptyMessage = 'Нет записей по текущим фильтрам.', emptyMessage = 'Нет записей по текущим фильтрам.',
className, className,
@@ -75,6 +82,8 @@ export function PreviewSectionGrid<T extends object>({
selectable?: boolean selectable?: boolean
bulkActions?: (selectedIds: string[], clear: () => void) => ReactNode bulkActions?: (selectedIds: string[], clear: () => void) => ReactNode
toolbar?: ReactNode toolbar?: ReactNode
/** Включает перетаскивание строк (dnd-kit). Передаёт id строки и id строки, на место которой её бросили. */
onReorder?: (activeId: string, overId: string) => void
pageSize?: number pageSize?: number
emptyMessage?: string emptyMessage?: string
className?: string className?: string
@@ -113,6 +122,16 @@ export function PreviewSectionGrid<T extends object>({
const defs = useMemo<ColumnDef<DataGridFeatures, T>[]>(() => { const defs = useMemo<ColumnDef<DataGridFeatures, T>[]>(() => {
const list: ColumnDef<DataGridFeatures, T>[] = [] const list: ColumnDef<DataGridFeatures, T>[] = []
if (onReorder) {
list.push({
id: 'drag',
header: () => <span className="sr-only">Порядок</span>,
cell: () => <DataGridTableDndRowHandle />,
enableSorting: false,
enableResizing: false,
size: DRAG_COLUMN_SIZE,
})
}
if (selectable) { if (selectable) {
list.push({ list.push({
id: 'select', id: 'select',
@@ -140,7 +159,7 @@ export function PreviewSectionGrid<T extends object>({
}) })
} }
return list return list
}, [columns, selectable]) }, [columns, selectable, onReorder])
const table = useTable({ const table = useTable({
features: dataGridFeatures, features: dataGridFeatures,
@@ -154,7 +173,17 @@ export function PreviewSectionGrid<T extends object>({
onPaginationChange: setPagination, onPaginationChange: setPagination,
}) })
const minWidth = columns.reduce((sum, c) => sum + c.size, selectable ? SELECT_COLUMN_SIZE : 0) const dataIds = table.getRowModel().rows.map((r) => r.id)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!onReorder || !active || !over || active.id === over.id) return
onReorder(String(active.id), String(over.id))
}
const minWidth = columns.reduce(
(sum, c) => sum + c.size,
(selectable ? SELECT_COLUMN_SIZE : 0) + (onReorder ? DRAG_COLUMN_SIZE : 0),
)
const selectedIds = table.getSelectedRowModel().rows.map((r) => r.id) const selectedIds = table.getSelectedRowModel().rows.map((r) => r.id)
const paged = data.length > pageSize const paged = data.length > pageSize
@@ -165,7 +194,7 @@ export function PreviewSectionGrid<T extends object>({
recordCount={data.length} recordCount={data.length}
onRowClick={onRowClick} onRowClick={onRowClick}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
tableLayout={{ headerSticky: false, dense: true, width: 'auto' }} tableLayout={{ headerSticky: false, dense: true, width: 'auto', rowsDraggable: Boolean(onReorder) }}
> >
<Frame spacing="sm" className={cn('w-full min-w-0', className)}> <Frame spacing="sm" className={cn('w-full min-w-0', className)}>
<FrameHeader> <FrameHeader>
@@ -226,7 +255,11 @@ export function PreviewSectionGrid<T extends object>({
) : null} ) : null}
<DataGridScrollArea> <DataGridScrollArea>
<div className="pb-3" style={{ minWidth }}> <div className="pb-3" style={{ minWidth }}>
<DataGridTable /> {onReorder ? (
<DataGridTableDndRows dataIds={dataIds} handleDragEnd={handleDragEnd} />
) : (
<DataGridTable />
)}
</div> </div>
</DataGridScrollArea> </DataGridScrollArea>
<Separator /> <Separator />