feat:generic DataTable(0.4.0)
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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="Заявок пока нет"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user