mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Implement DataTable features
This commit is contained in:
parent
86c2965820
commit
38cdf31676
|
@ -27,9 +27,9 @@ class LibraryActiveView(generics.ListAPIView):
|
||||||
# pylint: disable=unsupported-binary-operation
|
# pylint: disable=unsupported-binary-operation
|
||||||
return m.LibraryItem.objects.filter(
|
return m.LibraryItem.objects.filter(
|
||||||
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
|
Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)
|
||||||
).distinct()
|
).distinct().order_by('-time_update')
|
||||||
else:
|
else:
|
||||||
return m.LibraryItem.objects.filter(is_common=True)
|
return m.LibraryItem.objects.filter(is_common=True).order_by('-time_update')
|
||||||
|
|
||||||
|
|
||||||
class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
class ConstituentAPIView(generics.RetrieveUpdateAPIView):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { CheckboxChecked } from '../Icons';
|
import { CheckboxCheckedIcon } from '../Icons';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
export interface CheckboxProps
|
export interface CheckboxProps
|
||||||
|
@ -53,7 +53,7 @@ function Checkbox({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={`max-w-[1rem] min-w-[1rem] h-4 mt-0.5 border rounded-sm ${bgColor} ${cursor}`} >
|
<div className={`max-w-[1rem] min-w-[1rem] h-4 mt-0.5 border rounded-sm ${bgColor} ${cursor}`} >
|
||||||
{ value && <div className='mt-[1px] ml-[1px]'><CheckboxChecked /></div>}
|
{ value && <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div>}
|
||||||
</div>
|
</div>
|
||||||
{ label &&
|
{ label &&
|
||||||
<Label
|
<Label
|
||||||
|
|
|
@ -1,265 +0,0 @@
|
||||||
import { Cell, flexRender, getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
Header, HeaderGroup, Row, RowData, TableOptions, useReactTable
|
|
||||||
} from '@tanstack/react-table';
|
|
||||||
|
|
||||||
import Checkbox from './Checkbox';
|
|
||||||
import Tristate from './Tristate';
|
|
||||||
|
|
||||||
export interface DataTableProps<TData extends RowData>
|
|
||||||
extends Omit<TableOptions<TData>, 'getCoreRowModel' | 'getSortedRowModel'| 'getPaginationRowModel'> {
|
|
||||||
onRowClicked?: (row: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
|
||||||
onRowDoubleClicked?: (row: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
|
||||||
noDataComponent?: React.ReactNode
|
|
||||||
pagination?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultNoDataComponent() {
|
|
||||||
return (
|
|
||||||
<div className='p-2 text-center'>
|
|
||||||
Данные отсутствуют
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DataTable<TData extends RowData>({
|
|
||||||
onRowClicked, onRowDoubleClicked, noDataComponent=defaultNoDataComponent(),
|
|
||||||
enableRowSelection, enableMultiRowSelection,
|
|
||||||
pagination,
|
|
||||||
...options
|
|
||||||
}: DataTableProps<TData>) {
|
|
||||||
// const [sorting, setSorting] = React.useState<SortingState>([])
|
|
||||||
|
|
||||||
const tableImpl = useReactTable({
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: options.enableSorting ? getSortedRowModel() : undefined,
|
|
||||||
getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
|
|
||||||
|
|
||||||
state: {
|
|
||||||
...options.state
|
|
||||||
},
|
|
||||||
// onSortingChange: setSorting,
|
|
||||||
enableRowSelection: enableRowSelection,
|
|
||||||
enableMultiRowSelection: enableMultiRowSelection,
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
|
|
||||||
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='w-full'>
|
|
||||||
{isEmpty && noDataComponent}
|
|
||||||
{!isEmpty &&
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
{tableImpl.getHeaderGroups().map(
|
|
||||||
(headerGroup: HeaderGroup<TData>) => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{(enableRowSelection ?? enableMultiRowSelection) &&
|
|
||||||
<th className='pl-3 pr-1'>
|
|
||||||
<Tristate
|
|
||||||
tabIndex={-1}
|
|
||||||
value={
|
|
||||||
!tableImpl.getIsAllPageRowsSelected() && tableImpl.getIsSomePageRowsSelected() ? null :
|
|
||||||
tableImpl.getIsAllPageRowsSelected()
|
|
||||||
}
|
|
||||||
tooltip='Выделить все'
|
|
||||||
setValue={value => tableImpl.toggleAllPageRowsSelected(value !== false)}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
}
|
|
||||||
{headerGroup.headers.map(
|
|
||||||
(header: Header<TData, unknown>) => (
|
|
||||||
<th key={header.id}
|
|
||||||
colSpan={header.colSpan}
|
|
||||||
className='p-2 text-xs font-semibold select-none whitespace-nowrap'
|
|
||||||
style={{
|
|
||||||
textAlign: header.getSize() > 100 ? 'left': 'center',
|
|
||||||
width: header.getSize()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* {header.isPlaceholder ? null : (
|
|
||||||
<div
|
|
||||||
{...{
|
|
||||||
className: header.column.getCanSort()
|
|
||||||
? 'cursor-pointer select-none'
|
|
||||||
: '',
|
|
||||||
onClick: header.column.getToggleSortingHandler(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
{{
|
|
||||||
asc: ' 🔼',
|
|
||||||
desc: ' 🔽',
|
|
||||||
}[header.column.getIsSorted() as string] ?? null}
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
{header.isPlaceholder ? null
|
|
||||||
: flexRender(header.column.columnDef.header, header.getContext())
|
|
||||||
}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{tableImpl.getRowModel().rows.map(
|
|
||||||
(row: Row<TData>) => (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={
|
|
||||||
row.getIsSelected() ? 'clr-selected clr-hover' :
|
|
||||||
row.index % 2 === 0 ? 'clr-controls clr-hover' :
|
|
||||||
'clr-app clr-hover'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(enableRowSelection ?? enableMultiRowSelection) &&
|
|
||||||
<td className='pl-3 pr-1 border-y'>
|
|
||||||
<Checkbox
|
|
||||||
tabIndex={-1}
|
|
||||||
value={row.getIsSelected()}
|
|
||||||
setValue={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
}
|
|
||||||
{row.getVisibleCells().map(
|
|
||||||
(cell: Cell<TData, unknown>) => (
|
|
||||||
<td
|
|
||||||
key={cell.id}
|
|
||||||
className='px-2 py-1 border-y'
|
|
||||||
style={{
|
|
||||||
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto'
|
|
||||||
}}
|
|
||||||
onClick={event => onRowClicked && onRowClicked(row.original, event)}
|
|
||||||
onDoubleClick={event => onRowDoubleClicked && onRowDoubleClicked(row.original, event)}
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
|
|
||||||
<tfoot>
|
|
||||||
{tableImpl.getFooterGroups().map(
|
|
||||||
(footerGroup: HeaderGroup<TData>) => (
|
|
||||||
<tr key={footerGroup.id}>
|
|
||||||
{footerGroup.headers.map(
|
|
||||||
(header: Header<TData, unknown>) => (
|
|
||||||
<th key={header.id}>
|
|
||||||
{header.isPlaceholder ? null
|
|
||||||
: flexRender(header.column.columnDef.footer, header.getContext())
|
|
||||||
}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tfoot>
|
|
||||||
</table>}
|
|
||||||
{/*
|
|
||||||
<div className="h-2" />
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
className="p-1 border rounded"
|
|
||||||
onClick={() => table.setPageIndex(0)}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
{'<<'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-1 border rounded"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
{'<'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-1 border rounded"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
{'>'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-1 border rounded"
|
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
{'>>'}
|
|
||||||
</button>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div>Page</div>
|
|
||||||
<strong>
|
|
||||||
{table.getState().pagination.pageIndex + 1} of{' '}
|
|
||||||
{table.getPageCount()}
|
|
||||||
</strong>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
| Go to page:
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
|
||||||
onChange={e => {
|
|
||||||
const page = e.target.value ? Number(e.target.value) - 1 : 0
|
|
||||||
table.setPageIndex(page)
|
|
||||||
}}
|
|
||||||
className="w-16 p-1 border rounded"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<select
|
|
||||||
value={table.getState().pagination.pageSize}
|
|
||||||
onChange={e => {
|
|
||||||
table.setPageSize(Number(e.target.value))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{[10, 20, 30, 40, 50].map(pageSize => (
|
|
||||||
<option key={pageSize} value={pageSize}>
|
|
||||||
Show {pageSize}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="h-4" />
|
|
||||||
<button onClick={() => rerender()} className="p-2 border">
|
|
||||||
Rerender
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) */}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// import { TableOptions, useReactTable } from '@tanstack/react-table'
|
|
||||||
|
|
||||||
// import { useConceptTheme } from '../../context/ThemeContext';
|
|
||||||
// import { dataTableDarkT, dataTableLightT } from '../../utils/color';
|
|
||||||
|
|
||||||
// export interface SelectionInfo<T> {
|
|
||||||
// allSelected: boolean
|
|
||||||
// selectedCount: number
|
|
||||||
// selectedRows: T[]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// interface DataTableProps<T>
|
|
||||||
// extends TableOptions<T>{}
|
|
||||||
|
|
||||||
// function DataTable<T>({ ...props }: DataTableProps<T>) {
|
|
||||||
// const { darkMode } = useConceptTheme();
|
|
||||||
// const table = useReactTable(props);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <DataTable<T>
|
|
||||||
// theme={ theme ?? (darkMode ? 'customDark' : 'customLight')}
|
|
||||||
// paginationComponentOptions={{
|
|
||||||
// rowsPerPageText: 'строк на страницу'
|
|
||||||
// }}
|
|
||||||
// {...props}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default DataTable;
|
|
|
@ -5,10 +5,7 @@ interface DividerProps {
|
||||||
|
|
||||||
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
|
function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={`${margins} ${vertical ? 'border-x-2 h-full': 'border-y-2 w-full'}`} />
|
||||||
{vertical && <div className={`${margins} border-x-2`} />}
|
|
||||||
{!vertical && <div className={`${margins} border-y-2`} />}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { CheckboxChecked, CheckboxNull } from '../Icons';
|
import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
|
||||||
import { CheckboxProps } from './Checkbox';
|
import { CheckboxProps } from './Checkbox';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
|
@ -54,8 +54,8 @@ function Tristate({
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={`w-4 h-4 shrink-0 mt-0.5 border rounded-sm ${bgColor} ${cursor}`} >
|
<div className={`w-4 h-4 shrink-0 mt-0.5 border rounded-sm ${bgColor} ${cursor}`} >
|
||||||
{ value && <div className='mt-[1px] ml-[1px]'><CheckboxChecked /></div>}
|
{ value && <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div>}
|
||||||
{ value == null && <div className='mt-[1px] ml-[1px]'><CheckboxNull /></div>}
|
{ value == null && <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div>}
|
||||||
</div>
|
</div>
|
||||||
{ label &&
|
{ label &&
|
||||||
<Label
|
<Label
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
function defaultNoDataComponent() {
|
||||||
|
return (
|
||||||
|
<div className='p-2 text-center'>
|
||||||
|
Данные отсутствуют
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultNoDataComponent;
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { Table } from '@tanstack/react-table';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { GotoFirstIcon, GotoLastIcon, GotoNextIcon, GotoPrevIcon } from '../Icons';
|
||||||
|
|
||||||
|
interface PaginationToolsProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
paginationOptions: number[]
|
||||||
|
onChangePaginationOption?: (newValue: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) {
|
||||||
|
const handlePaginationOptionsChange = useCallback(
|
||||||
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const perPage = Number(event.target.value);
|
||||||
|
table.setPageSize(perPage);
|
||||||
|
if (onChangePaginationOption) {
|
||||||
|
onChangePaginationOption(perPage);
|
||||||
|
}
|
||||||
|
}, [onChangePaginationOption, table]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='text-sm flex justify-end w-full items-center text-controls select-none my-2'>
|
||||||
|
<div className='flex items-center gap-1 mr-3'>
|
||||||
|
<div className=''>
|
||||||
|
{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
|
||||||
|
-
|
||||||
|
{Math.min(table.getFilteredRowModel().rows.length, (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize)}
|
||||||
|
</div>
|
||||||
|
<span>из</span>
|
||||||
|
<div className=''>{table.getFilteredRowModel().rows.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex'>
|
||||||
|
<button
|
||||||
|
className='clr-hover text-controls'
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<GotoFirstIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='clr-hover text-controls'
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<GotoPrevIcon />
|
||||||
|
</button>
|
||||||
|
<input type='text'
|
||||||
|
title='Номер страницы. Выделите для ручного ввода'
|
||||||
|
className='w-6 clr-app text-center'
|
||||||
|
value={table.getState().pagination.pageIndex + 1}
|
||||||
|
onChange={event => {
|
||||||
|
const page = event.target.value ? Number(event.target.value) - 1 : 0;
|
||||||
|
if (page + 1 <= table.getPageCount()) {
|
||||||
|
table.setPageIndex(page);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='clr-hover text-controls'
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<GotoNextIcon />
|
||||||
|
</button>
|
||||||
|
<button className='clr-hover text-controls'
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<GotoLastIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={table.getState().pagination.pageSize}
|
||||||
|
onChange={handlePaginationOptionsChange}
|
||||||
|
className='clr-app mx-2 cursor-pointer'
|
||||||
|
>
|
||||||
|
{paginationOptions.map(
|
||||||
|
pageSize => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
{pageSize} на стр
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PaginationTools;
|
23
rsconcept/frontend/src/components/DataTable/SelectAll.tsx
Normal file
23
rsconcept/frontend/src/components/DataTable/SelectAll.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Table } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import Tristate from '../Common/Tristate';
|
||||||
|
|
||||||
|
interface SelectAllProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectAll<TData>({ table }: SelectAllProps<TData>) {
|
||||||
|
return (
|
||||||
|
<Tristate
|
||||||
|
tabIndex={-1}
|
||||||
|
value={
|
||||||
|
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null :
|
||||||
|
table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
tooltip='Выделить все'
|
||||||
|
setValue={value => table.toggleAllPageRowsSelected(value !== false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectAll;
|
19
rsconcept/frontend/src/components/DataTable/SelectRow.tsx
Normal file
19
rsconcept/frontend/src/components/DataTable/SelectRow.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import Checkbox from '../Common/Checkbox';
|
||||||
|
|
||||||
|
interface SelectRowProps<TData> {
|
||||||
|
row: Row<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectRow<TData>({ row }: SelectRowProps<TData>) {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
tabIndex={-1}
|
||||||
|
value={row.getIsSelected()}
|
||||||
|
setValue={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectRow;
|
19
rsconcept/frontend/src/components/DataTable/SortingIcon.tsx
Normal file
19
rsconcept/frontend/src/components/DataTable/SortingIcon.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { AscendingIcon, DescendingIcon } from '../Icons';
|
||||||
|
|
||||||
|
interface SortingIconProps<TData> {
|
||||||
|
column: Column<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
|
||||||
|
return (<>
|
||||||
|
{{
|
||||||
|
desc: <DescendingIcon size={4} />,
|
||||||
|
asc: <AscendingIcon size={4}/>,
|
||||||
|
}[column.getIsSorted() as string] ??
|
||||||
|
<DescendingIcon size={4} color='opacity-0 hover:opacity-50' />}
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortingIcon;
|
201
rsconcept/frontend/src/components/DataTable/index.tsx
Normal file
201
rsconcept/frontend/src/components/DataTable/index.tsx
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import {
|
||||||
|
Cell, ColumnSort,
|
||||||
|
createColumnHelper, flexRender, getCoreRowModel,
|
||||||
|
getPaginationRowModel, getSortedRowModel, Header, HeaderGroup,
|
||||||
|
PaginationState, Row, RowData, type RowSelectionState,
|
||||||
|
SortingState, TableOptions, useReactTable, type VisibilityState
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import DefaultNoData from './DefaultNoData';
|
||||||
|
import PaginationTools from './PaginationTools';
|
||||||
|
import SelectAll from './SelectAll';
|
||||||
|
import SelectRow from './SelectRow';
|
||||||
|
import SortingIcon from './SortingIcon';
|
||||||
|
|
||||||
|
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
|
||||||
|
|
||||||
|
export interface IConditionalStyle<TData> {
|
||||||
|
when: (rowData: TData) => boolean
|
||||||
|
style: React.CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableProps<TData extends RowData>
|
||||||
|
extends Pick<TableOptions<TData>,
|
||||||
|
'data' | 'columns' |
|
||||||
|
'onRowSelectionChange' | 'onColumnVisibilityChange'
|
||||||
|
> {
|
||||||
|
dense?: boolean
|
||||||
|
conditionalRowStyles?: IConditionalStyle<TData>[]
|
||||||
|
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
||||||
|
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
|
||||||
|
noDataComponent?: React.ReactNode
|
||||||
|
|
||||||
|
enableRowSelection?: boolean
|
||||||
|
rowSelection?: RowSelectionState
|
||||||
|
|
||||||
|
enableHiding?: boolean
|
||||||
|
columnVisibility?: VisibilityState
|
||||||
|
|
||||||
|
enablePagination?: boolean
|
||||||
|
paginationPerPage?: number
|
||||||
|
paginationOptions?: number[]
|
||||||
|
onChangePaginationOption?: (newValue: number) => void
|
||||||
|
|
||||||
|
enableSorting?: boolean
|
||||||
|
initialSorting?: ColumnSort
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DataTable<TData extends RowData>({
|
||||||
|
dense, conditionalRowStyles,
|
||||||
|
onRowClicked, onRowDoubleClicked, noDataComponent,
|
||||||
|
|
||||||
|
enableRowSelection,
|
||||||
|
rowSelection,
|
||||||
|
|
||||||
|
enableHiding,
|
||||||
|
columnVisibility,
|
||||||
|
|
||||||
|
enableSorting,
|
||||||
|
initialSorting,
|
||||||
|
|
||||||
|
enablePagination,
|
||||||
|
paginationPerPage=10,
|
||||||
|
paginationOptions=[10, 20, 30, 40, 50],
|
||||||
|
onChangePaginationOption,
|
||||||
|
|
||||||
|
...options
|
||||||
|
}: DataTableProps<TData>) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
|
||||||
|
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: paginationPerPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableImpl = useReactTable({
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
|
||||||
|
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
||||||
|
|
||||||
|
state: {
|
||||||
|
pagination: pagination,
|
||||||
|
sorting: sorting,
|
||||||
|
rowSelection: rowSelection ?? {},
|
||||||
|
columnVisibility: columnVisibility ?? {}
|
||||||
|
},
|
||||||
|
enableHiding: enableHiding,
|
||||||
|
onPaginationChange: enablePagination ? setPagination : undefined,
|
||||||
|
onSortingChange: enableSorting ? setSorting : undefined,
|
||||||
|
enableMultiRowSelection: enableRowSelection,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEmpty = tableImpl.getRowModel().rows.length === 0;
|
||||||
|
|
||||||
|
function getRowStyles(row: Row<TData>) {
|
||||||
|
return {...conditionalRowStyles!
|
||||||
|
.filter(item => item.when(row.original))
|
||||||
|
.reduce((prev, item) => ({...prev, ...item.style}), {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
{isEmpty && (noDataComponent ?? <DefaultNoData />)}
|
||||||
|
|
||||||
|
{!isEmpty &&
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
{tableImpl.getHeaderGroups().map(
|
||||||
|
(headerGroup: HeaderGroup<TData>) => (
|
||||||
|
<tr key={headerGroup.id}>
|
||||||
|
{enableRowSelection &&
|
||||||
|
<th className='pl-3 pr-1'>
|
||||||
|
<SelectAll table={tableImpl} />
|
||||||
|
</th>}
|
||||||
|
{headerGroup.headers.map(
|
||||||
|
(header: Header<TData, unknown>) => (
|
||||||
|
<th key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
|
||||||
|
style={{
|
||||||
|
textAlign: header.getSize() > 100 ? 'left': 'center',
|
||||||
|
width: header.getSize(),
|
||||||
|
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto'
|
||||||
|
}}
|
||||||
|
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<div className='flex gap-1'>
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
{enableSorting && header.column.getCanSort() && <SortingIcon column={header.column} />}
|
||||||
|
</div>)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{tableImpl.getRowModel().rows.map(
|
||||||
|
(row: Row<TData>, index) => (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={
|
||||||
|
row.getIsSelected() ? 'clr-selected clr-hover' :
|
||||||
|
index % 2 === 0 ? 'clr-controls clr-hover' :
|
||||||
|
'clr-app clr-hover'
|
||||||
|
}
|
||||||
|
style={conditionalRowStyles && getRowStyles(row)}
|
||||||
|
>
|
||||||
|
{enableRowSelection &&
|
||||||
|
<td className='pl-3 pr-1 border-y'>
|
||||||
|
<SelectRow row={row} />
|
||||||
|
</td>}
|
||||||
|
{row.getVisibleCells().map(
|
||||||
|
(cell: Cell<TData, unknown>) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className='px-2 border-y'
|
||||||
|
style={{
|
||||||
|
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto',
|
||||||
|
paddingBottom: dense ? '0.25rem': '0.5rem',
|
||||||
|
paddingTop: dense ? '0.25rem': '0.5rem'
|
||||||
|
}}
|
||||||
|
onClick={event => onRowClicked && onRowClicked(row.original, event)}
|
||||||
|
onDoubleClick={event => onRowDoubleClicked && onRowDoubleClicked(row.original, event)}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
<tfoot>
|
||||||
|
{tableImpl.getFooterGroups().map(
|
||||||
|
(footerGroup: HeaderGroup<TData>) => (
|
||||||
|
<tr key={footerGroup.id}>
|
||||||
|
{footerGroup.headers.map(
|
||||||
|
(header: Header<TData, unknown>) => (
|
||||||
|
<th key={header.id}>
|
||||||
|
{header.isPlaceholder ? null
|
||||||
|
: flexRender(header.column.columnDef.footer, header.getContext())
|
||||||
|
}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tfoot>
|
||||||
|
</table>}
|
||||||
|
|
||||||
|
{enablePagination &&
|
||||||
|
<PaginationTools
|
||||||
|
table={tableImpl}
|
||||||
|
paginationOptions={paginationOptions}
|
||||||
|
onChangePaginationOption={onChangePaginationOption}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>);
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ function HelpTermGraph() {
|
||||||
<p><b>Клик на конституенту</b> - выделение, включая скрытые конституенты</p>
|
<p><b>Клик на конституенту</b> - выделение, включая скрытые конституенты</p>
|
||||||
<p><b>Двойной клик</b> - редактирование конституенты</p>
|
<p><b>Двойной клик</b> - редактирование конституенты</p>
|
||||||
<p><b>Delete</b> - удалить выбранные</p>
|
<p><b>Delete</b> - удалить выбранные</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
<Divider margins='mt-2' />
|
<Divider margins='mt-2' />
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) {
|
||||||
<svg
|
<svg
|
||||||
width={width}
|
width={width}
|
||||||
height={width}
|
height={width}
|
||||||
className={`w-[${width}] h-[${width}] ${color ?? ''}`}
|
className={`w-[${width}] h-[${width}] ${color}`}
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
viewBox={viewbox}
|
viewBox={viewbox}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -309,7 +309,7 @@ export function MeshIcon(props: IconProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InDoor(props: IconProps) {
|
export function InDoorIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<IconSVG viewbox='0 0 24 24' {...props}>
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
<path fill='none' d='M0 0h24v24H0z' />
|
<path fill='none' d='M0 0h24v24H0z' />
|
||||||
|
@ -318,7 +318,55 @@ export function InDoor(props: IconProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckboxChecked() {
|
export function GotoLastIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M7.707 17.707L13.414 12 7.707 6.293 6.293 7.707 10.586 12l-4.293 4.293zM15 6h2v12h-2z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GotoFirstIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M16.293 17.707l1.414-1.414L13.414 12l4.293-4.293-1.414-1.414L10.586 12zM7 6h2v12H7z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GotoNextIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M10.707 17.707L16.414 12l-5.707-5.707-1.414 1.414L13.586 12l-4.293 4.293z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GotoPrevIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M13.293 6.293L7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DescendingIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M11.998 17l7-8h-14z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AscendingIcon(props: IconProps) {
|
||||||
|
return (
|
||||||
|
<IconSVG viewbox='0 0 24 24' {...props}>
|
||||||
|
<path d='M5 15h14l-7-8z' />
|
||||||
|
</IconSVG>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckboxCheckedIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className='w-3 h-3'
|
className='w-3 h-3'
|
||||||
|
@ -330,7 +378,7 @@ export function CheckboxChecked() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckboxNull() {
|
export function CheckboxNullIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className='w-3 h-3'
|
className='w-3 h-3'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useConceptNavigation } from '../../context/NagivationContext';
|
import { useConceptNavigation } from '../../context/NagivationContext';
|
||||||
import useDropdown from '../../hooks/useDropdown';
|
import useDropdown from '../../hooks/useDropdown';
|
||||||
import { InDoor, UserIcon } from '../Icons';
|
import { InDoorIcon, UserIcon } from '../Icons';
|
||||||
import NavigationButton from './NavigationButton';
|
import NavigationButton from './NavigationButton';
|
||||||
import UserDropdown from './UserDropdown';
|
import UserDropdown from './UserDropdown';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ function UserMenu() {
|
||||||
<NavigationButton
|
<NavigationButton
|
||||||
text='Войти...'
|
text='Войти...'
|
||||||
description='Перейти на страницу логина'
|
description='Перейти на страницу логина'
|
||||||
icon={<InDoor />}
|
icon={<InDoorIcon />}
|
||||||
onClick={navigateLogin}
|
onClick={navigateLogin}
|
||||||
/>}
|
/>}
|
||||||
{ user &&
|
{ user &&
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
--cl-bg-60: hsl(000, 000%, 091%);
|
--cl-bg-60: hsl(000, 000%, 091%);
|
||||||
--cl-bg-40: hsl(000, 000%, 080%);
|
--cl-bg-40: hsl(000, 000%, 080%);
|
||||||
|
|
||||||
--cl-fg-60: hsl(000, 000%, 055%);
|
--cl-fg-60: hsl(000, 000%, 065%);
|
||||||
--cl-fg-80: hsl(000, 000%, 047%);
|
--cl-fg-80: hsl(000, 000%, 047%);
|
||||||
--cl-fg-100: hsl(000, 000%, 000%);
|
--cl-fg-100: hsl(000, 000%, 000%);
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
|
||||||
value={strategy}
|
value={strategy}
|
||||||
onChange={handleChangeStrategy}
|
onChange={handleChangeStrategy}
|
||||||
/>
|
/>
|
||||||
<div className='relative w-96 min-w-[10rem]'>
|
<div className='relative w-96 min-w-[10rem] select-none'>
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
|
||||||
<MagnifyingGlassIcon />
|
<MagnifyingGlassIcon />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { createColumnHelper } from '@tanstack/react-table';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||||
import DataTable from '../../components/Common/DataTable';
|
|
||||||
import TextURL from '../../components/Common/TextURL';
|
import TextURL from '../../components/Common/TextURL';
|
||||||
|
import DataTable, { createColumnHelper } from '../../components/DataTable';
|
||||||
import HelpLibrary from '../../components/Help/HelpLibrary';
|
import HelpLibrary from '../../components/Help/HelpLibrary';
|
||||||
import { EducationIcon, EyeIcon, GroupIcon, HelpIcon } from '../../components/Icons';
|
import { EducationIcon, EyeIcon, GroupIcon, HelpIcon } from '../../components/Icons';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import { useConceptNavigation } from '../../context/NagivationContext';
|
import { useConceptNavigation } from '../../context/NagivationContext';
|
||||||
import { useUsers } from '../../context/UsersContext';
|
import { useUsers } from '../../context/UsersContext';
|
||||||
|
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||||
import { prefixes } from '../../utils/constants';
|
import { prefixes } from '../../utils/constants';
|
||||||
import { ILibraryItem } from '../../utils/models';
|
import { ILibraryItem } from '../../utils/models';
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { getUserLabel } = useUsers();
|
const { getUserLabel } = useUsers();
|
||||||
|
|
||||||
|
const [ itemsPerPage, setItemsPerPage ] = useLocalStorage<number>('library_per_page', 50);
|
||||||
|
|
||||||
const openRSForm = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
|
const openRSForm = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
|
@ -34,6 +36,7 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
id: 'status',
|
id: 'status',
|
||||||
header: '',
|
header: '',
|
||||||
size: 60,
|
size: 60,
|
||||||
|
minSize: 60,
|
||||||
maxSize: 60,
|
maxSize: 60,
|
||||||
cell: props => {
|
cell: props => {
|
||||||
const item = props.row.original;
|
const item = props.row.original;
|
||||||
|
@ -55,34 +58,38 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
size: 200,
|
size: 200,
|
||||||
minSize: 200,
|
minSize: 200,
|
||||||
maxSize: 200,
|
maxSize: 200,
|
||||||
enableSorting: true
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('title', {
|
columnHelper.accessor('title', {
|
||||||
id: 'title',
|
id: 'title',
|
||||||
header: 'Название',
|
header: 'Название',
|
||||||
minSize: 200,
|
|
||||||
size: 1000,
|
size: 1000,
|
||||||
|
minSize: 400,
|
||||||
maxSize: 1000,
|
maxSize: 1000,
|
||||||
enableSorting: true
|
enableSorting: true,
|
||||||
|
sortingFn: 'text'
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(item => item.owner ?? 0, {
|
columnHelper.accessor(item => item.owner ?? 0, {
|
||||||
id: 'owner',
|
id: 'owner',
|
||||||
header: 'Владелец',
|
header: 'Владелец',
|
||||||
|
size: 300,
|
||||||
|
minSize: 200,
|
||||||
|
maxSize: 300,
|
||||||
cell: props => getUserLabel(props.cell.getValue()),
|
cell: props => getUserLabel(props.cell.getValue()),
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableResizing: false,
|
sortingFn: 'text'
|
||||||
minSize: 200,
|
|
||||||
size: 300,
|
|
||||||
maxSize: 300
|
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('time_update', {
|
columnHelper.accessor('time_update', {
|
||||||
id: 'time_update',
|
id: 'time_update',
|
||||||
header: 'Обновлена',
|
header: 'Обновлена',
|
||||||
minSize: 200,
|
size: 220,
|
||||||
size: 200,
|
minSize: 220,
|
||||||
maxSize: 200,
|
maxSize: 220,
|
||||||
cell: props => new Date(props.cell.getValue()).toLocaleString(intl.locale),
|
cell: props => new Date(props.cell.getValue()).toLocaleString(intl.locale),
|
||||||
enableSorting: true
|
enableSorting: true,
|
||||||
|
sortingFn: 'datetime',
|
||||||
|
sortDescFirst: true
|
||||||
})
|
})
|
||||||
], [intl, getUserLabel, user]);
|
], [intl, getUserLabel, user]);
|
||||||
|
|
||||||
|
@ -103,8 +110,6 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
// defaultSortFieldId='time_update'
|
|
||||||
// defaultSortAsc={false}
|
|
||||||
|
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[10rem]'>
|
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[10rem]'>
|
||||||
|
@ -119,11 +124,19 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
// pagination
|
|
||||||
// paginationPerPage={50}
|
|
||||||
// paginationRowsPerPageOptions={[10, 20, 30, 50, 100]}
|
|
||||||
onRowClicked={openRSForm}
|
onRowClicked={openRSForm}
|
||||||
|
|
||||||
|
enableSorting
|
||||||
|
initialSorting={{
|
||||||
|
id: 'time_update',
|
||||||
|
desc: true
|
||||||
|
}}
|
||||||
|
|
||||||
|
enablePagination
|
||||||
|
paginationPerPage={itemsPerPage}
|
||||||
|
onChangePaginationOption={setItemsPerPage}
|
||||||
|
paginationOptions={[10, 20, 30, 50, 100]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import TextArea from '../../components/Common/TextArea';
|
||||||
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
import HelpConstituenta from '../../components/Help/HelpConstituenta';
|
||||||
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
import { DumpBinIcon, HelpIcon, PenIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
import { CstType, EditMode, ICstCreateData, ICstRenameData, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
import { CstType, EditMode, ICstCreateData, ICstRenameData, ICstUpdateData, SyntaxTree } from '../../utils/models';
|
||||||
import { getCstTypificationLabel } from '../../utils/staticUI';
|
import { getCstTypificationLabel } from '../../utils/staticUI';
|
||||||
import EditorRSExpression from './EditorRSExpression';
|
import EditorRSExpression from './EditorRSExpression';
|
||||||
|
@ -17,6 +18,8 @@ import ViewSideConstituents from './elements/ViewSideConstituents';
|
||||||
// Max height of content for left enditor pane
|
// Max height of content for left enditor pane
|
||||||
const UNFOLDED_HEIGHT = '59.1rem';
|
const UNFOLDED_HEIGHT = '59.1rem';
|
||||||
|
|
||||||
|
const SIDELIST_HIDE_THRESHOLD = 1000;
|
||||||
|
|
||||||
interface EditorConstituentaProps {
|
interface EditorConstituentaProps {
|
||||||
activeID?: number
|
activeID?: number
|
||||||
onOpenEdit: (cstID: number) => void
|
onOpenEdit: (cstID: number) => void
|
||||||
|
@ -32,6 +35,7 @@ function EditorConstituenta({
|
||||||
isModified, setIsModified, activeID,
|
isModified, setIsModified, activeID,
|
||||||
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
|
onShowAST, onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
|
||||||
}: EditorConstituentaProps) {
|
}: EditorConstituentaProps) {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
const { schema, processing, isEditable, cstUpdate } = useRSForm();
|
const { schema, processing, isEditable, cstUpdate } = useRSForm();
|
||||||
const activeCst = useMemo(
|
const activeCst = useMemo(
|
||||||
() => {
|
() => {
|
||||||
|
@ -237,6 +241,7 @@ function EditorConstituenta({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD &&
|
||||||
<div className='self-stretch w-full pb-1 border'>
|
<div className='self-stretch w-full pb-1 border'>
|
||||||
<ViewSideConstituents
|
<ViewSideConstituents
|
||||||
expression={expression}
|
expression={expression}
|
||||||
|
@ -244,7 +249,7 @@ function EditorConstituenta({
|
||||||
activeID={activeID}
|
activeID={activeID}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import { createColumnHelper,RowSelectionState } from '@tanstack/react-table';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import Button from '../../components/Common/Button';
|
import Button from '../../components/Common/Button';
|
||||||
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
import ConceptTooltip from '../../components/Common/ConceptTooltip';
|
||||||
import DataTable from '../../components/Common/DataTable';
|
|
||||||
import Divider from '../../components/Common/Divider';
|
import Divider from '../../components/Common/Divider';
|
||||||
|
import DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../components/DataTable';
|
||||||
import HelpRSFormItems from '../../components/Help/HelpRSFormItems';
|
import HelpRSFormItems from '../../components/Help/HelpRSFormItems';
|
||||||
import { ArrowDownIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, MeshIcon, SmallPlusIcon } from '../../components/Icons';
|
import { ArrowDownIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, MeshIcon, SmallPlusIcon } from '../../components/Icons';
|
||||||
import { useRSForm } from '../../context/RSFormContext';
|
import { useRSForm } from '../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../context/ThemeContext';
|
import { useConceptTheme } from '../../context/ThemeContext';
|
||||||
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
import { prefixes } from '../../utils/constants';
|
import { prefixes } from '../../utils/constants';
|
||||||
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
|
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
|
||||||
import { getCstStatusFgColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
|
import { getCstStatusFgColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
|
||||||
|
|
||||||
|
// Window width cutoff for columns
|
||||||
|
const COLUMN_DEFINITION_HIDE_THRESHOLD = 1000;
|
||||||
|
const COLUMN_TYPE_HIDE_THRESHOLD = 1200;
|
||||||
|
const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
|
||||||
interface EditorItemsProps {
|
interface EditorItemsProps {
|
||||||
|
@ -24,11 +29,13 @@ interface EditorItemsProps {
|
||||||
|
|
||||||
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
|
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
|
||||||
const { colors } = useConceptTheme();
|
const { colors } = useConceptTheme();
|
||||||
|
const windowSize = useWindowSize();
|
||||||
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
|
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
|
||||||
const [selected, setSelected] = useState<number[]>([]);
|
const [selected, setSelected] = useState<number[]>([]);
|
||||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||||
|
|
||||||
// Delete selected constituents
|
// Delete selected constituents
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
|
@ -155,7 +162,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
case '2': handleCreateCst(CstType.STRUCTURED); return true;
|
case '2': handleCreateCst(CstType.STRUCTURED); return true;
|
||||||
case '3': handleCreateCst(CstType.TERM); return true;
|
case '3': handleCreateCst(CstType.TERM); return true;
|
||||||
case '4': handleCreateCst(CstType.AXIOM); return true;
|
case '4': handleCreateCst(CstType.AXIOM); return true;
|
||||||
|
case 'й':
|
||||||
case 'q': handleCreateCst(CstType.FUNCTION); return true;
|
case 'q': handleCreateCst(CstType.FUNCTION); return true;
|
||||||
|
case 'ц':
|
||||||
case 'w': handleCreateCst(CstType.PREDICATE); return true;
|
case 'w': handleCreateCst(CstType.PREDICATE); return true;
|
||||||
case '5': handleCreateCst(CstType.CONSTANT); return true;
|
case '5': handleCreateCst(CstType.CONSTANT); return true;
|
||||||
case '6': handleCreateCst(CstType.THEOREM); return true;
|
case '6': handleCreateCst(CstType.THEOREM); return true;
|
||||||
|
@ -176,6 +185,15 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onOpenEdit(cst.id);
|
onOpenEdit(cst.id);
|
||||||
}, [onOpenEdit]);
|
}, [onOpenEdit]);
|
||||||
|
|
||||||
|
useLayoutEffect(
|
||||||
|
() => {
|
||||||
|
setColumnVisibility({
|
||||||
|
'type': (windowSize.width ?? 0) >= COLUMN_TYPE_HIDE_THRESHOLD,
|
||||||
|
'convention': (windowSize.width ?? 0) >= COLUMN_CONVENTION_HIDE_THRESHOLD,
|
||||||
|
'definition': (windowSize.width ?? 0) >= COLUMN_DEFINITION_HIDE_THRESHOLD
|
||||||
|
});
|
||||||
|
}, [windowSize]);
|
||||||
|
|
||||||
useLayoutEffect(
|
useLayoutEffect(
|
||||||
() => {
|
() => {
|
||||||
|
@ -199,13 +217,14 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
header: 'Имя',
|
header: 'Имя',
|
||||||
size: 65,
|
size: 65,
|
||||||
minSize: 65,
|
minSize: 65,
|
||||||
|
maxSize: 65,
|
||||||
cell: props => {
|
cell: props => {
|
||||||
const cst = props.row.original;
|
const cst = props.row.original;
|
||||||
const info = mapStatusInfo.get(cst.status);
|
const info = mapStatusInfo.get(cst.status);
|
||||||
return (<>
|
return (<>
|
||||||
<div
|
<div
|
||||||
id={`${prefixes.cst_list}${cst.alias}`}
|
id={`${prefixes.cst_list}${cst.alias}`}
|
||||||
className='w-full px-1 text-center rounded-md whitespace-nowrap'
|
className='w-full min-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap'
|
||||||
style={{
|
style={{
|
||||||
borderWidth: "1px",
|
borderWidth: "1px",
|
||||||
borderColor: getCstStatusFgColor(cst.status, colors),
|
borderColor: getCstStatusFgColor(cst.status, colors),
|
||||||
|
@ -228,154 +247,145 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
|
||||||
columnHelper.accessor(cst => getCstTypificationLabel(cst), {
|
columnHelper.accessor(cst => getCstTypificationLabel(cst), {
|
||||||
id: 'type',
|
id: 'type',
|
||||||
header: 'Типизация',
|
header: 'Типизация',
|
||||||
size: 175,
|
size: 150,
|
||||||
maxSize: 175,
|
minSize: 150,
|
||||||
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
|
maxSize: 150,
|
||||||
|
enableHiding: true,
|
||||||
|
cell: props => <div className='text-sm min-w-[8.4rem]'>{props.getValue()}</div>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
|
columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
|
||||||
id: 'term',
|
id: 'term',
|
||||||
header: 'Термин',
|
header: 'Термин',
|
||||||
size: 350,
|
size: 500,
|
||||||
minSize: 150,
|
minSize: 150,
|
||||||
maxSize: 350
|
maxSize: 500
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('definition_formal', {
|
columnHelper.accessor('definition_formal', {
|
||||||
id: 'expression',
|
id: 'expression',
|
||||||
header: 'Формальное определение',
|
header: 'Формальное определение',
|
||||||
size: 300,
|
size: 1000,
|
||||||
minSize: 300,
|
minSize: 300,
|
||||||
maxSize: 500
|
maxSize: 1000
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
|
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
|
||||||
id: 'definition',
|
id: 'definition',
|
||||||
header: 'Текстовое определение',
|
header: 'Текстовое определение',
|
||||||
size: 200,
|
size: 1000,
|
||||||
minSize: 200,
|
minSize: 200,
|
||||||
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
|
maxSize: 1000,
|
||||||
|
cell: props => <div className='text-xs'>{props.getValue()}</div>
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('convention', {
|
columnHelper.accessor('convention', {
|
||||||
id: 'convention',
|
id: 'convention',
|
||||||
header: 'Конвенция / Комментарий',
|
header: 'Конвенция / Комментарий',
|
||||||
|
size: 500,
|
||||||
minSize: 100,
|
minSize: 100,
|
||||||
maxSize: undefined,
|
maxSize: 500,
|
||||||
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
|
enableHiding: true,
|
||||||
|
cell: props => <div className='text-xs'>{props.getValue()}</div>
|
||||||
}),
|
}),
|
||||||
], [colors]);
|
], [colors]);
|
||||||
|
|
||||||
// name: 'Типизация',
|
|
||||||
// hide: 1600
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Формальное определение',
|
|
||||||
// grow: 2,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Текстовое определение',
|
|
||||||
// grow: 2,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Конвенция / Комментарий',
|
|
||||||
// id: 'convention',
|
|
||||||
// hide: 1800
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div
|
<div
|
||||||
className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] select-none clr-app'
|
className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] select-none clr-app'
|
||||||
>
|
>
|
||||||
<div className='mr-3 whitespace-nowrap'>
|
<div className='mr-3 whitespace-nowrap'>
|
||||||
Выбраны
|
Выбраны
|
||||||
<span className='ml-2'>
|
<span className='ml-2'>
|
||||||
{selected.length} из {schema?.stats?.count_all ?? 0}
|
{selected.length} из {schema?.stats?.count_all ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-start w-full gap-1'>
|
|
||||||
<Button
|
|
||||||
tooltip='Переместить вверх'
|
|
||||||
icon={<ArrowUpIcon size={6}/>}
|
|
||||||
disabled={!isEditable || nothingSelected}
|
|
||||||
dense
|
|
||||||
onClick={handleMoveUp}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip='Переместить вниз'
|
|
||||||
icon={<ArrowDownIcon size={6}/>}
|
|
||||||
disabled={!isEditable || nothingSelected}
|
|
||||||
dense
|
|
||||||
onClick={handleMoveDown}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip='Удалить выбранные'
|
|
||||||
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={6}/>}
|
|
||||||
disabled={!isEditable || nothingSelected}
|
|
||||||
dense
|
|
||||||
onClick={handleDelete}
|
|
||||||
/>
|
|
||||||
<Divider vertical margins='my-1' />
|
|
||||||
<Button
|
|
||||||
tooltip='Сбросить имена'
|
|
||||||
icon={<MeshIcon color={isEditable ? 'text-primary': ''} size={6}/>}
|
|
||||||
dense
|
|
||||||
disabled={!isEditable}
|
|
||||||
onClick={handleReindex}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
tooltip='Новая конституента'
|
|
||||||
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} size={6}/>}
|
|
||||||
dense
|
|
||||||
disabled={!isEditable}
|
|
||||||
onClick={() => handleCreateCst()}
|
|
||||||
/>
|
|
||||||
{(Object.values(CstType)).map(
|
|
||||||
(typeStr) => {
|
|
||||||
const type = typeStr as CstType;
|
|
||||||
return (
|
|
||||||
<Button key={type}
|
|
||||||
text={getCstTypePrefix(type)}
|
|
||||||
tooltip={getCstTypeShortcut(type)}
|
|
||||||
dense
|
|
||||||
widthClass='w-[1.4rem]'
|
|
||||||
disabled={!isEditable}
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={() => handleCreateCst(type)}
|
|
||||||
/>);
|
|
||||||
})}
|
|
||||||
<div id='items-table-help'>
|
|
||||||
<HelpIcon color='text-primary' size={6} />
|
|
||||||
</div>
|
|
||||||
<ConceptTooltip anchorSelect='#items-table-help' offset={30}>
|
|
||||||
<HelpRSFormItems />
|
|
||||||
</ConceptTooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-full text-sm' onKeyDown={handleTableKey}>
|
<div className='flex items-center justify-start w-full gap-1'>
|
||||||
<DataTable
|
<Button
|
||||||
data={schema?.items ?? []}
|
tooltip='Переместить вверх'
|
||||||
columns={columns}
|
icon={<ArrowUpIcon size={6}/>}
|
||||||
state={{
|
disabled={!isEditable || nothingSelected}
|
||||||
rowSelection: rowSelection
|
dense
|
||||||
}}
|
onClick={handleMoveUp}
|
||||||
|
|
||||||
enableMultiRowSelection
|
|
||||||
|
|
||||||
onRowDoubleClicked={handleRowDoubleClicked}
|
|
||||||
onRowClicked={handleRowClicked}
|
|
||||||
onRowSelectionChange={setRowSelection}
|
|
||||||
|
|
||||||
noDataComponent={
|
|
||||||
<span className='flex flex-col justify-center p-2 text-center'>
|
|
||||||
<p>Список пуст</p>
|
|
||||||
<p
|
|
||||||
className='cursor-pointer text-primary hover:underline'
|
|
||||||
onClick={() => handleCreateCst()}>
|
|
||||||
Создать новую конституенту
|
|
||||||
</p>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
tooltip='Переместить вниз'
|
||||||
|
icon={<ArrowDownIcon size={6}/>}
|
||||||
|
disabled={!isEditable || nothingSelected}
|
||||||
|
dense
|
||||||
|
onClick={handleMoveDown}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
tooltip='Удалить выбранные'
|
||||||
|
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={6}/>}
|
||||||
|
disabled={!isEditable || nothingSelected}
|
||||||
|
dense
|
||||||
|
onClick={handleDelete}
|
||||||
|
/>
|
||||||
|
<Divider vertical margins='my-1' />
|
||||||
|
<Button
|
||||||
|
tooltip='Сбросить имена'
|
||||||
|
icon={<MeshIcon color={isEditable ? 'text-primary': ''} size={6}/>}
|
||||||
|
dense
|
||||||
|
disabled={!isEditable}
|
||||||
|
onClick={handleReindex}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
tooltip='Новая конституента'
|
||||||
|
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} size={6}/>}
|
||||||
|
dense
|
||||||
|
disabled={!isEditable}
|
||||||
|
onClick={() => handleCreateCst()}
|
||||||
|
/>
|
||||||
|
{(Object.values(CstType)).map(
|
||||||
|
(typeStr) => {
|
||||||
|
const type = typeStr as CstType;
|
||||||
|
return (
|
||||||
|
<Button key={type}
|
||||||
|
text={getCstTypePrefix(type)}
|
||||||
|
tooltip={getCstTypeShortcut(type)}
|
||||||
|
dense
|
||||||
|
widthClass='w-[1.4rem]'
|
||||||
|
disabled={!isEditable}
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => handleCreateCst(type)}
|
||||||
|
/>);
|
||||||
|
})}
|
||||||
|
<div id='items-table-help'>
|
||||||
|
<HelpIcon color='text-primary' size={6} />
|
||||||
|
</div>
|
||||||
|
<ConceptTooltip anchorSelect='#items-table-help' offset={30}>
|
||||||
|
<HelpRSFormItems />
|
||||||
|
</ConceptTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className='w-full h-full text-sm' onKeyDown={handleTableKey}>
|
||||||
|
<DataTable
|
||||||
|
data={schema?.items ?? []}
|
||||||
|
columns={columns}
|
||||||
|
dense
|
||||||
|
|
||||||
|
onRowDoubleClicked={handleRowDoubleClicked}
|
||||||
|
onRowClicked={handleRowClicked}
|
||||||
|
|
||||||
|
enableHiding
|
||||||
|
columnVisibility={columnVisibility}
|
||||||
|
onColumnVisibilityChange={setColumnVisibility}
|
||||||
|
|
||||||
|
enableRowSelection
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
onRowSelectionChange={setRowSelection}
|
||||||
|
|
||||||
|
noDataComponent={
|
||||||
|
<span className='flex flex-col justify-center p-2 text-center'>
|
||||||
|
<p>Список пуст</p>
|
||||||
|
<p
|
||||||
|
className='cursor-pointer text-primary hover:underline'
|
||||||
|
onClick={() => handleCreateCst()}>
|
||||||
|
Создать новую конституенту
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditorItems;
|
export default EditorItems;
|
||||||
|
|
|
@ -375,10 +375,10 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div className='flex items-center justify-between py-1'>
|
<div className='flex items-center justify-between py-1'>
|
||||||
<div className='mr-3 whitespace-nowrap'>
|
<div className='mr-3 whitespace-nowrap text-base'>
|
||||||
Выбраны
|
Выбраны
|
||||||
<span className='ml-1'>
|
<span className='ml-1'>
|
||||||
<b>{allSelected.length}</b> из {schema?.stats?.count_all ?? 0}
|
{allSelected.length} из {schema?.stats?.count_all ?? 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { createColumnHelper } from '@tanstack/react-table';
|
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import DataTable from '../../../components/Common/DataTable';
|
import DataTable, { createColumnHelper, IConditionalStyle, VisibilityState } from '../../../components/DataTable';
|
||||||
import { useRSForm } from '../../../context/RSFormContext';
|
import { useRSForm } from '../../../context/RSFormContext';
|
||||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||||
import useLocalStorage from '../../../hooks/useLocalStorage';
|
import useLocalStorage from '../../../hooks/useLocalStorage';
|
||||||
|
import useWindowSize from '../../../hooks/useWindowSize';
|
||||||
import { prefixes } from '../../../utils/constants';
|
import { prefixes } from '../../../utils/constants';
|
||||||
import { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models';
|
import { applyGraphFilter, CstMatchMode, CstType, DependencyMode, extractGlobals, IConstituenta, matchConstituenta } from '../../../utils/models';
|
||||||
import { getCstDescription, getCstStatusFgColor, getMockConstituenta } from '../../../utils/staticUI';
|
import { getCstDescription, getCstStatusFgColor, getMockConstituenta } from '../../../utils/staticUI';
|
||||||
|
@ -15,6 +15,9 @@ import MatchModePicker from './MatchModePicker';
|
||||||
// Height that should be left to accomodate navigation panel + bottom margin
|
// Height that should be left to accomodate navigation panel + bottom margin
|
||||||
const LOCAL_NAVIGATION_H = '2.1rem';
|
const LOCAL_NAVIGATION_H = '2.1rem';
|
||||||
|
|
||||||
|
// Window width cutoff for expression show
|
||||||
|
const COLUMN_EXPRESSION_HIDE_THRESHOLD = 1500;
|
||||||
|
|
||||||
interface ViewSideConstituentsProps {
|
interface ViewSideConstituentsProps {
|
||||||
expression: string
|
expression: string
|
||||||
baseHeight: string
|
baseHeight: string
|
||||||
|
@ -29,8 +32,11 @@ function isMockCst(cst: IConstituenta) {
|
||||||
const columnHelper = createColumnHelper<IConstituenta>();
|
const columnHelper = createColumnHelper<IConstituenta>();
|
||||||
|
|
||||||
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
const { noNavigation, colors } = useConceptTheme();
|
const { noNavigation, colors } = useConceptTheme();
|
||||||
const { schema } = useRSForm();
|
const { schema } = useRSForm();
|
||||||
|
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({'expression': true})
|
||||||
|
|
||||||
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
|
const [filterMatch, setFilterMatch] = useLocalStorage('side-filter-match', CstMatchMode.ALL);
|
||||||
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
|
const [filterText, setFilterText] = useLocalStorage('side-filter-text', '');
|
||||||
|
@ -38,7 +44,19 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
|
||||||
|
|
||||||
useEffect(
|
useLayoutEffect(
|
||||||
|
() => {
|
||||||
|
setColumnVisibility(prev => {
|
||||||
|
const newValue = (windowSize.width ?? 0) >= COLUMN_EXPRESSION_HIDE_THRESHOLD;
|
||||||
|
if (newValue === prev['expression']) {
|
||||||
|
return prev;
|
||||||
|
} else {
|
||||||
|
return {'expression': newValue}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [windowSize]);
|
||||||
|
|
||||||
|
useLayoutEffect(
|
||||||
() => {
|
() => {
|
||||||
if (!schema?.items) {
|
if (!schema?.items) {
|
||||||
setFilteredData([]);
|
setFilteredData([]);
|
||||||
|
@ -118,9 +136,9 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
columnHelper.accessor(cst => getCstDescription(cst), {
|
columnHelper.accessor(cst => getCstDescription(cst), {
|
||||||
id: 'description',
|
id: 'description',
|
||||||
header: 'Описание',
|
header: 'Описание',
|
||||||
size: 350,
|
size: 500,
|
||||||
minSize: 350,
|
minSize: 350,
|
||||||
maxSize: 350,
|
maxSize: 500,
|
||||||
cell: props =>
|
cell: props =>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
@ -132,9 +150,10 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
columnHelper.accessor('definition_formal', {
|
columnHelper.accessor('definition_formal', {
|
||||||
id: 'expression',
|
id: 'expression',
|
||||||
header: 'Выражение',
|
header: 'Выражение',
|
||||||
size: 700,
|
size: 1000,
|
||||||
minSize: 0,
|
minSize: 0,
|
||||||
maxSize: 700,
|
maxSize: 1000,
|
||||||
|
enableHiding: true,
|
||||||
cell: props =>
|
cell: props =>
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
@ -145,6 +164,16 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
})
|
})
|
||||||
], [colors]);
|
], [colors]);
|
||||||
|
|
||||||
|
const conditionalRowStyles = useMemo(
|
||||||
|
(): IConditionalStyle<IConstituenta>[] => [
|
||||||
|
{
|
||||||
|
when: (cst: IConstituenta) => cst.id === activeID,
|
||||||
|
style: {
|
||||||
|
backgroundColor: colors.bgSelected
|
||||||
|
},
|
||||||
|
}
|
||||||
|
], [activeID, colors]);
|
||||||
|
|
||||||
const maxHeight = useMemo(
|
const maxHeight = useMemo(
|
||||||
() => {
|
() => {
|
||||||
const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
|
const siblingHeight = `${baseHeight} - ${LOCAL_NAVIGATION_H}`
|
||||||
|
@ -174,9 +203,12 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
|
||||||
<DataTable
|
<DataTable
|
||||||
data={filteredData}
|
data={filteredData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
conditionalRowStyles={conditionalRowStyles}
|
||||||
|
dense
|
||||||
|
|
||||||
|
enableHiding
|
||||||
// conditionalRowStyles={conditionalRowStyles}
|
columnVisibility={columnVisibility}
|
||||||
|
onColumnVisibilityChange={setColumnVisibility}
|
||||||
|
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem]'>
|
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem]'>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { createColumnHelper } from '@tanstack/react-table';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
import DataTable from '../../components/Common/DataTable';
|
import DataTable, { createColumnHelper } from '../../components/DataTable';
|
||||||
import { useConceptNavigation } from '../../context/NagivationContext';
|
import { useConceptNavigation } from '../../context/NagivationContext';
|
||||||
import { ILibraryItem } from '../../utils/models';
|
import { ILibraryItem } from '../../utils/models';
|
||||||
|
|
||||||
|
@ -52,9 +51,13 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
// defaultSortFieldId='time_update'
|
|
||||||
// defaultSortAsc={false}
|
|
||||||
|
|
||||||
|
dense
|
||||||
|
enableSorting
|
||||||
|
initialSorting={{
|
||||||
|
id: 'time_update',
|
||||||
|
desc: true
|
||||||
|
}}
|
||||||
noDataComponent={
|
noDataComponent={
|
||||||
<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>
|
<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const lightT: IColorTheme = {
|
||||||
fgGreen: 'hsl(100, 090%, 035%)',
|
fgGreen: 'hsl(100, 090%, 035%)',
|
||||||
fgBlue: 'hsl(235, 100%, 050%)',
|
fgBlue: 'hsl(235, 100%, 050%)',
|
||||||
fgPurple: 'hsl(270, 100%, 070%)',
|
fgPurple: 'hsl(270, 100%, 070%)',
|
||||||
fgTeal: 'hsl(192, 090%, 040%)',
|
fgTeal: 'hsl(200, 080%, 050%)',
|
||||||
fgOrange: 'hsl(030, 090%, 055%)'
|
fgOrange: 'hsl(030, 090%, 055%)'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user