diff --git a/PROJECT.md b/PROJECT.md index c882600..1602eaa 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -83,6 +83,11 @@ RequestNumber, StatusBadge, ErrorOverlay, FormMode (FormModeProvider + useFormRe из attachments.service объявить локальный `{ url: string; fileName: string }`. - PrintButton → переносим КАК ЕСТЬ; сохраняем соглашение классов `.rp-print-root` / `.rp-no-print` — задокументировать в README, чтобы потребители их соблюдали. +- ItemsTable → [DataTable СДЕЛАН в 0.4.0] generic `DataTable` в 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?, align?,editable?,render?}, value/onChange/readOnly). Excel-вставка — ОПЦИОНАЛЬНАЯ/ подключаемая (не хардкодить parseExcelRows под ТМЦ; проект даёт свой маппинг/парсер). diff --git a/package.json b/package.json index 8d5af35..a78f76f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tnsu/ui-kit", - "version": "0.3.0", + "version": "0.4.0", "description": "TNSU shared UI components for SPFx web parts", "author": "Timur Gorodetskiy", "license": "ISC", diff --git a/src/DataTable/DataTable.module.scss b/src/DataTable/DataTable.module.scss new file mode 100644 index 0000000..717536f --- /dev/null +++ b/src/DataTable/DataTable.module.scss @@ -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; +} diff --git a/src/DataTable/DataTable.stories.tsx b/src/DataTable/DataTable.stories.tsx new file mode 100644 index 0000000..61b4871 --- /dev/null +++ b/src/DataTable/DataTable.stories.tsx @@ -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[] = [ + { key: 'id', title: 'ID', width: '64px' }, + { key: 'type', title: 'Тип' }, + { key: 'title', title: 'Название' }, + { + key: 'status', + title: 'Статус', + render: (r) => ( + + {r.status} + + ) + } + ]; + return ( + r.id} + onRowClick={(r) => window.alert(`Открыть заявку #${r.id}`)} + /> + ); + } +}; + +// --- Пример 2: редактируемая табличная часть (инпуты в ячейках) --- +interface IItem { + name: string; + qty: string; +} + +export const Editable: StoryObj = { + render: () => { + const [items, setItems] = useState([ + { 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[] = [ + { key: 'n', title: '№', width: '48px', render: (_r, i) => i + 1 }, + { + key: 'name', + title: 'Наименование', + render: (r, i) => ( + setCell(i, 'name', e.target.value)} style={{ width: '100%' }} /> + ) + }, + { + key: 'qty', + title: 'Кол-во', + align: 'right', + render: (r, i) => ( + setCell(i, 'qty', e.target.value)} style={{ width: '80px' }} /> + ) + } + ]; + return ; + } +}; + +// --- Пример 3: пустое состояние --- +export const Empty: StoryObj = { + render: () => ( + + ) +}; diff --git a/src/DataTable/DataTable.tsx b/src/DataTable/DataTable.tsx new file mode 100644 index 0000000..82f749e --- /dev/null +++ b/src/DataTable/DataTable.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import styles from './DataTable.module.scss'; + +export interface IDataTableColumn { + /** Идентификатор колонки (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 { + columns: IDataTableColumn[]; + 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({ + columns, + rows, + rowKey, + onRowClick, + emptyText = 'Нет данных' +}: IDataTableProps): JSX.Element { + const clickable = !!onRowClick; + + return ( +
+ + + + {columns.map((c) => ( + + ))} + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((row, i) => ( + onRowClick!(row) : undefined} + onKeyDown={ + clickable + ? (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onRowClick!(row); + } + } + : undefined + } + > + {columns.map((c) => ( + + ))} + + )) + )} + +
+ {c.title} +
+ {emptyText} +
+ {c.render ? c.render(row, i) : String((row as Record)[c.key] ?? '')} +
+
+ ); +} diff --git a/src/index.ts b/src/index.ts index 6d4ca61..56ac7d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,5 +53,9 @@ export type { ISidebarItem, ISidebarProps } from './Sidebar/Sidebar'; export { SidebarToggle } from './Sidebar/SidebarToggle'; export type { ISidebarToggleProps } from './Sidebar/SidebarToggle'; +// --- Таблицы --- +export { DataTable } from './DataTable/DataTable'; +export type { IDataTableColumn, IDataTableProps } from './DataTable/DataTable'; + // --- Хук анимации перестановки строк таблицы (FLIP) --- export { useFlipRows } from './useFlipRows';