feat:generic DataTable(0.4.0)

This commit is contained in:
Тимур Городецкий
2026-08-14 14:32:08 +05:00
parent d16e4a2216
commit e3a9b5360c
6 changed files with 265 additions and 1 deletions
+5
View File
@@ -83,6 +83,11 @@ RequestNumber, StatusBadge, ErrorOverlay, FormMode (FormModeProvider + useFormRe
из attachments.service объявить локальный `{ url: string; fileName: string }`. из attachments.service объявить локальный `{ url: string; fileName: string }`.
- PrintButton → переносим КАК ЕСТЬ; сохраняем соглашение классов `.rp-print-root` / - PrintButton → переносим КАК ЕСТЬ; сохраняем соглашение классов `.rp-print-root` /
`.rp-no-print` — задокументировать в README, чтобы потребители их соблюдали. `.rp-no-print` — задокументировать в README, чтобы потребители их соблюдали.
- ItemsTable → [DataTable СДЕЛАН в 0.4.0] generic `DataTable<T>` в kit: columns
({key,title,width,align,render}) + rows + rowKey + onRowClick + emptyText. Editable —
через render-инпуты (без спец-режима). Обвязка (поиск/пагинация/Excel/add-remove) — в
проекте. overflow-x на мобилке. Осталось перевести на него RequestsTableView и ItemsTable
в портале (обвязку оставить). Старый план ниже — для истории:
- ItemsTable → ОБОБЩАЕМ в generic `DataTable` (колонки через проп: {key,title,width?, - ItemsTable → ОБОБЩАЕМ в generic `DataTable` (колонки через проп: {key,title,width?,
align?,editable?,render?}, value/onChange/readOnly). Excel-вставка — ОПЦИОНАЛЬНАЯ/ align?,editable?,render?}, value/onChange/readOnly). Excel-вставка — ОПЦИОНАЛЬНАЯ/
подключаемая (не хардкодить parseExcelRows под ТМЦ; проект даёт свой маппинг/парсер). подключаемая (не хардкодить parseExcelRows под ТМЦ; проект даёт свой маппинг/парсер).
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@tnsu/ui-kit", "name": "@tnsu/ui-kit",
"version": "0.3.0", "version": "0.4.0",
"description": "TNSU shared UI components for SPFx web parts", "description": "TNSU shared UI components for SPFx web parts",
"author": "Timur Gorodetskiy", "author": "Timur Gorodetskiy",
"license": "ISC", "license": "ISC",
+58
View File
@@ -0,0 +1,58 @@
@import '../tokens/tokens';
// Обёртка с горизонтальным скроллом — на узких экранах таблица не ломает вёрстку,
// а прокручивается вбок.
.wrap {
width: 100%;
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-family: $font-sans;
font-size: $fs-sm;
th {
padding: 10px 14px;
text-align: left;
font-size: $fs-overline;
font-weight: 600;
letter-spacing: .6px;
text-transform: uppercase;
color: $gray-500;
border-bottom: 1px solid $gray-200;
white-space: nowrap;
}
td {
padding: 12px 14px;
color: $gray-900;
border-bottom: 1px solid $gray-100;
vertical-align: middle;
}
tbody tr:last-child td {
border-bottom: 0;
}
}
.rowClickable {
cursor: pointer;
transition: background .14s;
&:hover {
background: $navy-50;
}
&:focus-visible {
outline: 2px solid $gold-400;
outline-offset: -2px;
}
}
.empty {
padding: 24px 14px;
text-align: center;
color: $gray-400;
}
+104
View File
@@ -0,0 +1,104 @@
import * as React from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import { DataTable, IDataTableColumn } from './DataTable';
import { Badge } from '../Badge/Badge';
const meta: Meta = {
title: 'Таблицы/DataTable',
tags: ['autodocs']
};
export default meta;
// --- Пример 1: список (только чтение, кликабельные строки, бейдж в ячейке) ---
interface IRequestRow {
id: number;
type: string;
title: string;
status: string;
}
const REQUESTS: IRequestRow[] = [
{ id: 3, type: 'Заявка на отпуск', title: 'Отпуск — Тимур Городецкий', status: 'Согласовано' },
{ id: 7, type: 'Заявка в отдел 1С', title: 'ТЕСТ СОГЛАСОВАНИЯ', status: 'На согласовании' },
{ id: 18, type: 'Рабочее место', title: 'Раб. место — Тестов Т.Т.', status: 'Выполнение' }
];
export const RequestList: StoryObj = {
render: () => {
const columns: IDataTableColumn<IRequestRow>[] = [
{ key: 'id', title: 'ID', width: '64px' },
{ key: 'type', title: 'Тип' },
{ key: 'title', title: 'Название' },
{
key: 'status',
title: 'Статус',
render: (r) => (
<Badge variant={r.status === 'Согласовано' ? 'success' : r.status === 'Выполнение' ? 'primary' : 'info'}>
{r.status}
</Badge>
)
}
];
return (
<DataTable
columns={columns}
rows={REQUESTS}
rowKey={(r) => r.id}
onRowClick={(r) => window.alert(`Открыть заявку #${r.id}`)}
/>
);
}
};
// --- Пример 2: редактируемая табличная часть (инпуты в ячейках) ---
interface IItem {
name: string;
qty: string;
}
export const Editable: StoryObj = {
render: () => {
const [items, setItems] = useState<IItem[]>([
{ name: 'Ноутбук', qty: '2' },
{ name: 'Мышь', qty: '5' }
]);
const setCell = (i: number, key: keyof IItem, value: string): void =>
setItems(items.map((r, idx) => (idx === i ? { ...r, [key]: value } : r)));
const columns: IDataTableColumn<IItem>[] = [
{ key: 'n', title: '№', width: '48px', render: (_r, i) => i + 1 },
{
key: 'name',
title: 'Наименование',
render: (r, i) => (
<input value={r.name} onChange={(e) => setCell(i, 'name', e.target.value)} style={{ width: '100%' }} />
)
},
{
key: 'qty',
title: 'Кол-во',
align: 'right',
render: (r, i) => (
<input value={r.qty} onChange={(e) => setCell(i, 'qty', e.target.value)} style={{ width: '80px' }} />
)
}
];
return <DataTable columns={columns} rows={items} />;
}
};
// --- Пример 3: пустое состояние ---
export const Empty: StoryObj = {
render: () => (
<DataTable
columns={[
{ key: 'id', title: 'ID' },
{ key: 'title', title: 'Название' }
]}
rows={[]}
emptyText="Заявок пока нет"
/>
)
};
+93
View File
@@ -0,0 +1,93 @@
import * as React from 'react';
import styles from './DataTable.module.scss';
export interface IDataTableColumn<T> {
/** Идентификатор колонки (React key + доступ к полю row[key] без render). */
key: string;
/** Заголовок колонки. '' / undefined — без заголовка (напр. колонка действий). */
title?: React.ReactNode;
/** Ширина: '64px' | '20%'. Без неё — авто. */
width?: string;
align?: 'left' | 'center' | 'right';
/** Кастомная ячейка (бейдж, инпут, ссылка…). Без render — показывается row[key]. */
render?: (row: T, index: number) => React.ReactNode;
}
export interface IDataTableProps<T> {
columns: IDataTableColumn<T>[];
rows: T[];
/** React key строки. По умолчанию — индекс. */
rowKey?: (row: T, index: number) => string | number;
/** Клик по строке (вся строка кликабельна + доступ с клавиатуры Enter/Space). */
onRowClick?: (row: T) => void;
/** Что показать, когда строк нет. */
emptyText?: React.ReactNode;
}
/**
* Универсальная таблица: колонки задаются пропсами, ячейка может рисоваться
* кастомно (render) — так одна таблица покрывает и список (бейджи, кликабельные
* строки), и редактируемую табличную часть (инпуты в ячейках). Поиск, пагинация,
* добавление строк, вставка из Excel — это обвязка, она остаётся в проекте.
* На узких экранах таблица прокручивается по горизонтали (overflow-x).
*/
export function DataTable<T>({
columns,
rows,
rowKey,
onRowClick,
emptyText = 'Нет данных'
}: IDataTableProps<T>): JSX.Element {
const clickable = !!onRowClick;
return (
<div className={styles.wrap}>
<table className={styles.table}>
<thead>
<tr>
{columns.map((c) => (
<th key={c.key} style={{ width: c.width, textAlign: c.align }}>
{c.title}
</th>
))}
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td className={styles.empty} colSpan={columns.length}>
{emptyText}
</td>
</tr>
) : (
rows.map((row, i) => (
<tr
key={rowKey ? rowKey(row, i) : i}
className={clickable ? styles.rowClickable : undefined}
tabIndex={clickable ? 0 : undefined}
role={clickable ? 'button' : undefined}
onClick={clickable ? () => onRowClick!(row) : undefined}
onKeyDown={
clickable
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onRowClick!(row);
}
}
: undefined
}
>
{columns.map((c) => (
<td key={c.key} style={{ textAlign: c.align }}>
{c.render ? c.render(row, i) : String((row as Record<string, unknown>)[c.key] ?? '')}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
);
}
+4
View File
@@ -53,5 +53,9 @@ export type { ISidebarItem, ISidebarProps } from './Sidebar/Sidebar';
export { SidebarToggle } from './Sidebar/SidebarToggle'; export { SidebarToggle } from './Sidebar/SidebarToggle';
export type { ISidebarToggleProps } from './Sidebar/SidebarToggle'; export type { ISidebarToggleProps } from './Sidebar/SidebarToggle';
// --- Таблицы ---
export { DataTable } from './DataTable/DataTable';
export type { IDataTableColumn, IDataTableProps } from './DataTable/DataTable';
// --- Хук анимации перестановки строк таблицы (FLIP) --- // --- Хук анимации перестановки строк таблицы (FLIP) ---
export { useFlipRows } from './useFlipRows'; export { useFlipRows } from './useFlipRows';