Refactoring: replacing data table component pt1

This commit is contained in:
IRBorisov 2023-09-09 20:36:55 +03:00
parent 82801e81c0
commit 86c2965820
28 changed files with 683 additions and 478 deletions

View File

@ -19,11 +19,11 @@ This readme file is used mostly to document project dependencies
- js-file-download - js-file-download
- react-tabs - react-tabs
- react-intl - react-intl
- react-data-table-component
- react-select - react-select
- react-error-boundary - react-error-boundary
- reagraph - reagraph
- react-tooltip - react-tooltip
- @tanstack/react-table
- @uiw/react-codemirror - @uiw/react-codemirror
- @uiw/codemirror-themes - @uiw/codemirror-themes
- @lezer/lr - @lezer/lr

View File

@ -9,12 +9,12 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@lezer/lr": "^1.3.10", "@lezer/lr": "^1.3.10",
"@tanstack/react-table": "^8.9.7",
"@uiw/codemirror-themes": "^4.21.13", "@uiw/codemirror-themes": "^4.21.13",
"@uiw/react-codemirror": "^4.21.13", "@uiw/react-codemirror": "^4.21.13",
"axios": "^1.5.0", "axios": "^1.5.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.2.0", "react": "^18.2.0",
"react-data-table-component": "^7.5.4",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11", "react-error-boundary": "^4.0.11",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
@ -3931,6 +3931,37 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@tanstack/react-table": {
"version": "8.9.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.9.7.tgz",
"integrity": "sha512-UKUekM8JNUyWbjT1q3s1GpH5OtBL9mJ4258Il23fsahvkh3ou9TuFVmqI0/UPiFROgHkRlCBDNPUhcsC9YPFgg==",
"dependencies": {
"@tanstack/table-core": "8.9.7"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.9.7",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.9.7.tgz",
"integrity": "sha512-lkhVcGDxa9GSoDFPkplPDvzsiUACPZrxT3U1edPs0DCMKFhBDgZ7d1DPd7cqHH0JoybfbQ/qiTQYOQBg8sinJg==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tweenjs/tween.js": { "node_modules/@tweenjs/tween.js": {
"version": "18.6.4", "version": "18.6.4",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-18.6.4.tgz",
@ -5477,6 +5508,7 @@
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -9318,18 +9350,6 @@
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/react-data-table-component": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/react-data-table-component/-/react-data-table-component-7.5.4.tgz",
"integrity": "sha512-6DGVj3urJZfEEMuP652fSjxdRVKeyb+9d0YounVc+MX8jwoyXQW6KO10eyZqElE9QtVrKrCeJxR7vht9yxyJiw==",
"dependencies": {
"deepmerge": "^4.2.2"
},
"peerDependencies": {
"react": ">= 16.8.3",
"styled-components": ">= 4"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",

View File

@ -13,12 +13,12 @@
}, },
"dependencies": { "dependencies": {
"@lezer/lr": "^1.3.10", "@lezer/lr": "^1.3.10",
"@tanstack/react-table": "^8.9.7",
"@uiw/codemirror-themes": "^4.21.13", "@uiw/codemirror-themes": "^4.21.13",
"@uiw/react-codemirror": "^4.21.13", "@uiw/react-codemirror": "^4.21.13",
"axios": "^1.5.0", "axios": "^1.5.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.2.0", "react": "^18.2.0",
"react-data-table-component": "^7.5.4",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11", "react-error-boundary": "^4.0.11",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",

View File

@ -27,7 +27,7 @@ function Button({
disabled={disabled ?? loading} disabled={disabled ?? loading}
onClick={onClick} onClick={onClick}
title={tooltip} title={tooltip}
className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${borderClass} ${colorClass} ${widthClass} ${cursor}`} className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${colorClass} ${widthClass} ${borderClass} ${cursor}`}
{...props} {...props}
> >
{icon && <span>{icon}</span>} {icon && <span>{icon}</span>}

View File

@ -3,7 +3,8 @@ import { useMemo } from 'react';
import { CheckboxChecked } from '../Icons'; import { CheckboxChecked } from '../Icons';
import Label from './Label'; import Label from './Label';
export interface CheckboxProps { export interface CheckboxProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'value' | 'onClick' > {
id?: string id?: string
label?: string label?: string
required?: boolean required?: boolean
@ -15,7 +16,10 @@ export interface CheckboxProps {
setValue?: (newValue: boolean) => void setValue?: (newValue: boolean) => void
} }
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: CheckboxProps) { function Checkbox({
id, required, disabled, tooltip, label,
widthClass = 'w-fit', value, setValue, ...props
}: CheckboxProps) {
const cursor = useMemo( const cursor = useMemo(
() => { () => {
if (disabled) { if (disabled) {
@ -46,8 +50,11 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit'
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
{...props}
> >
<div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${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>}
</div>
{ label && { label &&
<Label <Label
className={`${cursor} px-2 text-start`} className={`${cursor} px-2 text-start`}
@ -55,7 +62,6 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit'
required={required} required={required}
htmlFor={id} htmlFor={id}
/>} />}
{value && <CheckboxChecked />}
</button> </button>
); );
} }

View File

@ -1,32 +0,0 @@
import DataTable, { createTheme, type TableProps } from 'react-data-table-component';
import { useConceptTheme } from '../../context/ThemeContext';
import { dataTableDarkT, dataTableLightT } from '../../utils/color';
export interface SelectionInfo<T> {
allSelected: boolean
selectedCount: number
selectedRows: T[]
}
createTheme('customDark', dataTableDarkT, 'dark');
createTheme('customLight', dataTableLightT, 'light');
interface ConceptDataTableProps<T>
extends Omit<TableProps<T>, 'paginationComponentOptions'> {}
function ConceptDataTable<T>({ theme, ...props }: ConceptDataTableProps<T>) {
const { darkMode } = useConceptTheme();
return (
<DataTable<T>
theme={ theme ?? (darkMode ? 'customDark' : 'customLight')}
paginationComponentOptions={{
rowsPerPageText: 'строк на страницу'
}}
{...props}
/>
);
}
export default ConceptDataTable;

View File

@ -17,34 +17,27 @@ function ConceptSelectSingle<
> ({ ...props }: ConceptSelectSingleProps<Option, Group>) { > ({ ...props }: ConceptSelectSingleProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo( const themeColors = useMemo(
() => { () => !darkMode ? selectLightT : selectDarkT
return !darkMode ? selectLightT : selectDarkT; , [darkMode]);
}, [darkMode]);
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo( const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
() => { () => ({
return { control: (styles, { isDisabled }) => ({
control: (styles, { isDisabled }) => { ...styles,
return { borderRadius: '0.25rem',
...styles, cursor: isDisabled ? 'not-allowed' : 'pointer'
borderRadius: '0.25rem', }),
cursor: isDisabled ? 'not-allowed' : 'pointer' option: (styles, { isSelected }) => ({
}; ...styles,
}, backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
option: (styles, { isSelected }) => { color: isSelected ? colors.fgSelected : styles.color,
return { borderWidth: '1px',
...styles, borderColor: colors.border
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor, }),
color: isSelected ? colors.fgSelected : styles.color, input: (styles) => ({...styles}),
borderWidth: '1px', placeholder: (styles) => ({...styles}),
borderColor: colors.border singleValue: (styles) => ({...styles}),
}; }), [colors]);
},
input: (styles) => ({...styles}),
placeholder: (styles) => ({...styles}),
singleValue: (styles) => ({...styles}),
};
}, [colors]);
return ( return (
<Select <Select

View File

@ -12,7 +12,7 @@ function ConceptTooltip({ className, layer, place='bottom', ...props }: ConceptT
return ( return (
<Tooltip <Tooltip
opacity={0.95} opacity={0.97}
className={`overflow-auto border shadow-md ${layer ?? 'z-tooltip'} ${className}`} className={`overflow-auto border shadow-md ${layer ?? 'z-tooltip'} ${className}`}
variant={(darkMode ? 'dark' : 'light')} variant={(darkMode ? 'dark' : 'light')}
place={place} place={place}

View File

@ -0,0 +1,265 @@
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;

View File

@ -21,7 +21,7 @@ function TextArea({
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'> <div className='flex flex-col items-start [&:not(:first-child)]:mt-3'>
<Label <Label
text={label} text={label}
required={required} required={!props.disabled && required}
htmlFor={id} htmlFor={id}
/> />
<textarea id={id} <textarea id={id}

View File

@ -20,7 +20,7 @@ function TextInput({
<div className={`flex [&:not(:first-child)]:mt-3 ${singleRow ? 'items-center gap-4 ' + widthClass : 'flex-col items-start'}`}> <div className={`flex [&:not(:first-child)]:mt-3 ${singleRow ? 'items-center gap-4 ' + widthClass : 'flex-col items-start'}`}>
<Label <Label
text={label} text={label}
required={required} required={!props.disabled && required}
htmlFor={id} htmlFor={id}
/> />
<input id={id} <input id={id}

View File

@ -11,7 +11,10 @@ extends Omit<CheckboxProps, 'value' | 'setValue'> {
setValue?: (newValue: boolean | null) => void setValue?: (newValue: boolean | null) => void
} }
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: TristateProps) { function Tristate({
id, required, disabled, tooltip, label,
widthClass = 'w-fit', value, setValue, ...props
}: TristateProps) {
const cursor = useMemo( const cursor = useMemo(
() => { () => {
if (disabled) { if (disabled) {
@ -33,9 +36,11 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit'
return; return;
} }
if (value === false) { if (value === false) {
setValue(null); setValue(null);
} else if (value === null) {
setValue(true);
} else { } else {
setValue(!value); setValue(false);
} }
} }
@ -46,8 +51,12 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit'
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
{...props}
> >
<div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${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 == null && <div className='mt-[1px] ml-[1px]'><CheckboxNull /></div>}
</div>
{ label && { label &&
<Label <Label
className={`${cursor} px-2 text-start`} className={`${cursor} px-2 text-start`}
@ -55,10 +64,8 @@ function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit'
required={required} required={required}
htmlFor={id} htmlFor={id}
/>} />}
{value && <CheckboxChecked />}
{value === null && <CheckboxNull />}
</button> </button>
); );
} }
export default Checkbox; export default Tristate;

View File

@ -1,6 +1,6 @@
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { getCstStatusColor, mapStatusInfo } from '../../utils/staticUI'; import { getCstStatusBgColor, mapStatusInfo } from '../../utils/staticUI';
interface InfoCstStatusProps { interface InfoCstStatusProps {
title?: string title?: string
@ -18,7 +18,7 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span <span
className='px-1 inline-block font-semibold min-w-[5rem] text-center border text-sm' className='px-1 inline-block font-semibold min-w-[5rem] text-center border text-sm'
style={{backgroundColor: getCstStatusColor(status, colors)}} style={{backgroundColor: getCstStatusBgColor(status, colors)}}
> >
{info.text} {info.text}
</span> </span>

View File

@ -321,7 +321,7 @@ export function InDoor(props: IconProps) {
export function CheckboxChecked() { export function CheckboxChecked() {
return ( return (
<svg <svg
className='absolute w-3 h-3 mt-1 ml-0.5' className='w-3 h-3'
viewBox='0 0 512 512' viewBox='0 0 512 512'
fill='#ffffff' fill='#ffffff'
> >
@ -333,8 +333,8 @@ export function CheckboxChecked() {
export function CheckboxNull() { export function CheckboxNull() {
return ( return (
<svg <svg
className='absolute w-3 h-3 mt-1 ml-0.5' className='w-3 h-3'
viewBox='0 0 512 512' viewBox='0 0 16 16'
fill='#ffffff' fill='#ffffff'
> >
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' /> <path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />

View File

@ -7,18 +7,18 @@
:root { :root {
/* Light Theme */ /* Light Theme */
--cl-bg-120: hsl(000, 000%, 100%); --cl-bg-120: hsl(000, 000%, 100%);
--cl-bg-100: hsl(220, 020%, 098%); --cl-bg-100: hsl(000, 000%, 098%);
--cl-bg-80: hsl(220, 014%, 096%); --cl-bg-80: hsl(000, 000%, 094%);
--cl-bg-60: hsl(220, 013%, 091%); --cl-bg-60: hsl(000, 000%, 091%);
--cl-bg-40: hsl(216, 012%, 084%); --cl-bg-40: hsl(000, 000%, 080%);
--cl-fg-60: hsl(000, 000%, 055%); --cl-fg-60: hsl(000, 000%, 055%);
--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%);
--cl-prim-bg-100: hsl(220, 100%, 060%); --cl-prim-bg-100: hsl(220, 100%, 060%);
--cl-prim-bg-80: hsl(220, 100%, 090%); --cl-prim-bg-80: hsl(220, 080%, 092%);
--cl-prim-bg-60: hsl(220, 100%, 094%); --cl-prim-bg-60: hsl(190, 080%, 094%);
--cl-prim-fg-80: hsl(220, 100%, 050%); --cl-prim-fg-80: hsl(220, 100%, 050%);
--cl-prim-fg-100: hsl(000, 000%, 100%); --cl-prim-fg-100: hsl(000, 000%, 100%);
@ -38,24 +38,16 @@
--cd-fg-80: hsl(000, 000%, 080%); --cd-fg-80: hsl(000, 000%, 080%);
--cd-fg-100: hsl(000, 000%, 093%); --cd-fg-100: hsl(000, 000%, 093%);
/* --cd-prim-bg-100: hsl(025, 079%, 052%); --cd-prim-bg-100: hsl(267, 050%, 050%);
--cd-prim-bg-80: hsl(035, 080%, 043%); --cd-prim-bg-80: hsl(267, 050%, 032%);
--cd-prim-bg-60: hsl(045, 080%, 031%); --cd-prim-bg-60: hsl(269, 030%, 028%);
--cd-prim-fg-80: hsl(025, 080%, 050%); --cd-prim-fg-80: hsl(267, 070%, 070%);
--cd-prim-fg-100: hsl(000, 000%, 100%); */ --cd-prim-fg-100: hsl(000, 000%, 100%);
--cd-prim-bg-100: hsl(267, 50%, 50%);
--cd-prim-bg-80: hsl(267, 50%, 35%);
--cd-prim-bg-60: hsl(269, 50%, 20%);
--cd-prim-fg-60: hsl(267, 50%, 35%);
--cd-prim-fg-80: hsl(267, 50%, 50%);
--cd-prim-fg-100: #ffffff;
--cd-red-bg-100: hsl(000, 100%, 015%); --cd-red-bg-100: hsl(000, 100%, 015%);
--cd-red-fg-100: hsl(000, 100%, 060%); --cd-red-fg-100: hsl(000, 080%, 055%);
--cd-green-fg-100: hsl(120, 80%, 40%); --cd-green-fg-100: hsl(120, 080%, 040%);
/* Import overrides */ /* Import overrides */
--toastify-color-dark: var(--cd-bg-60); --toastify-color-dark: var(--cd-bg-60);
@ -246,14 +238,10 @@
} }
} }
.clr-btn-nav { :is(.text-controls,
color: var(--cl-fg-80); .clr-btn-nav,
.dark & { .clr-btn-clear
color: var(--cd-fg-80); ) {
}
}
.clr-btn-clear {
color: var(--cl-fg-80); color: var(--cl-fg-80);
&:disabled { &:disabled {
color: var(--cl-fg-60); color: var(--cl-fg-60);

View File

@ -26,7 +26,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
return ( return (
<div ref={pickerMenu.ref} className='h-full text-right'> <div ref={pickerMenu.ref} className='h-full text-right'>
<Button <Button
icon={<FilterCogIcon size={6} />} icon={<FilterCogIcon color='text-controls' size={6} />}
dense dense
tooltip='Фильтры' tooltip='Фильтры'
colorClass='clr-input clr-hover text-btn' colorClass='clr-input clr-hover text-btn'

View File

@ -37,16 +37,14 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) { function handleChangeQuery(event: React.ChangeEvent<HTMLInputElement>) {
const newQuery = event.target.value; const newQuery = event.target.value;
setQuery(newQuery); setQuery(newQuery);
setFilter(prev => { setFilter(prev => ({
return { query: newQuery,
query: newQuery, is_owned: prev.is_owned,
is_owned: prev.is_owned, is_common: prev.is_common,
is_common: prev.is_common, is_canonical: prev.is_canonical,
is_canonical: prev.is_canonical, is_subscribed: prev.is_subscribed,
is_subscribed: prev.is_subscribed, is_personal: prev.is_personal
is_personal: prev.is_personal }));
};
});
} }
useLayoutEffect(() => { useLayoutEffect(() => {
@ -83,7 +81,7 @@ function SearchPanel({ total, filtered, setFilter }: SearchPanelProps) {
onChange={handleChangeStrategy} onChange={handleChangeStrategy}
/> />
<div className='relative w-96 min-w-[10rem]'> <div className='relative w-96 min-w-[10rem]'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'> <div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon /> <MagnifyingGlassIcon />
</div> </div>
<input <input

View File

@ -1,8 +1,9 @@
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 ConceptDataTable from '../../components/Common/ConceptDataTable';
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 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';
@ -17,6 +18,8 @@ interface ViewLibraryProps {
cleanQuery: () => void cleanQuery: () => void
} }
const columnHelper = createColumnHelper<ILibraryItem>();
function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) { function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
const { navigateTo } = useConceptNavigation(); const { navigateTo } = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
@ -27,12 +30,13 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ columnHelper.display({
name: '',
id: 'status', id: 'status',
minWidth: '60px', header: '',
maxWidth: '60px', size: 60,
cell: (item: ILibraryItem) => { maxSize: 60,
cell: props => {
const item = props.row.original;
return (<> return (<>
<div <div
className='flex items-center justify-start gap-1' className='flex items-center justify-start gap-1'
@ -44,51 +48,50 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
</div> </div>
</>); </>);
}, },
sortable: true, }),
reorder: true columnHelper.accessor('alias', {
},
{
name: 'Шифр',
id: 'alias', id: 'alias',
maxWidth: '140px', header: 'Шифр',
selector: (item: ILibraryItem) => item.alias, size: 200,
sortable: true, minSize: 200,
reorder: true maxSize: 200,
}, enableSorting: true
{ }),
name: 'Название', columnHelper.accessor('title', {
id: 'title', id: 'title',
minWidth: '50%', header: 'Название',
selector: (item: ILibraryItem) => item.title, minSize: 200,
sortable: true, size: 1000,
reorder: true maxSize: 1000,
}, enableSorting: true
{ }),
name: 'Владелец', columnHelper.accessor(item => item.owner ?? 0, {
id: 'owner', id: 'owner',
selector: (item: ILibraryItem) => item.owner ?? 0, header: 'Владелец',
format: (item: ILibraryItem) => { cell: props => getUserLabel(props.cell.getValue()),
return getUserLabel(item.owner); enableSorting: true,
}, enableResizing: false,
sortable: true, minSize: 200,
reorder: true size: 300,
}, maxSize: 300
{ }),
name: 'Обновлена', columnHelper.accessor('time_update', {
id: 'time_update', id: 'time_update',
selector: (item: ILibraryItem) => item.time_update, header: 'Обновлена',
format: (item: ILibraryItem) => new Date(item.time_update).toLocaleString(intl.locale), minSize: 200,
sortable: true, size: 200,
reorder: true maxSize: 200,
} cell: props => new Date(props.cell.getValue()).toLocaleString(intl.locale),
enableSorting: true
})
], [intl, getUserLabel, user]); ], [intl, getUserLabel, user]);
return ( return (
<div> <div>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 left-0 flex gap-1 mt-1 ml-5 z-pop'> <div className='absolute top-[-0.125rem] left-0 flex gap-1 ml-3 z-pop'>
<div id='library-help' className='py-2'> <div id='library-help' className='py-2'>
<HelpIcon color='text-primary' size={6} /> <HelpIcon color='text-primary' size={5} />
</div> </div>
<ConceptTooltip anchorSelect='#library-help'> <ConceptTooltip anchorSelect='#library-help'>
<div className='max-w-[35rem]'> <div className='max-w-[35rem]'>
@ -97,14 +100,11 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
</ConceptTooltip> </ConceptTooltip>
</div> </div>
</div> </div>
<ConceptDataTable <DataTable
columns={columns} columns={columns}
data={items} data={items}
defaultSortFieldId='time_update' // defaultSortFieldId='time_update'
defaultSortAsc={false} // defaultSortAsc={false}
striped
highlightOnHover
pointerOnHover
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]'>
@ -120,10 +120,9 @@ function ViewLibrary({ items, cleanQuery }: ViewLibraryProps) {
</p> </p>
</div>} </div>}
// pagination
pagination // paginationPerPage={50}
paginationPerPage={50} // paginationRowsPerPageOptions={[10, 20, 30, 50, 100]}
paginationRowsPerPageOptions={[10, 20, 30, 50, 100]}
onRowClicked={openRSForm} onRowClicked={openRSForm}
/> />
</div> </div>

View File

@ -18,18 +18,15 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
const { darkMode, colors } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
const [hoverID, setHoverID] = useState<number | undefined>(undefined); const [hoverID, setHoverID] = useState<number | undefined>(undefined);
const hoverNode = useMemo( const hoverNode = useMemo(
() => { () => syntaxTree.find(node => node.uid === hoverID)
return syntaxTree.find(node => node.uid === hoverID); , [hoverID, syntaxTree]);
}, [hoverID, syntaxTree]);
const nodes: GraphNode[] = useMemo( const nodes: GraphNode[] = useMemo(
() => syntaxTree.map(node => { () => syntaxTree.map(node => ({
return { id: String(node.uid),
id: String(node.uid), label: getASTNodeLabel(node),
label: getASTNodeLabel(node), fill: getASTNodeColor(node, colors),
fill: getASTNodeColor(node, colors), })), [syntaxTree, colors]);
};
}), [syntaxTree, colors]);
const edges: GraphEdge[] = useMemo( const edges: GraphEdge[] = useMemo(
() => { () => {
@ -47,14 +44,12 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
}, [syntaxTree]); }, [syntaxTree]);
const handleHoverIn = useCallback( const handleHoverIn = useCallback(
(node: GraphNode) => { (node: GraphNode) => setHoverID(Number(node.id))
setHoverID(Number(node.id)); , []);
}, []);
const handleHoverOut = useCallback( const handleHoverOut = useCallback(
() => { () => setHoverID(undefined)
setHoverID(undefined); , []);
}, []);
return ( return (
<Modal <Modal

View File

@ -1,9 +1,10 @@
import { useCallback, useMemo, useState } from 'react'; import { createColumnHelper,RowSelectionState } from '@tanstack/react-table';
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 ConceptDataTable from '../../components/Common/ConceptDataTable';
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 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';
@ -11,7 +12,9 @@ import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
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 { getCstStatusColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI'; import { getCstStatusFgColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
const columnHelper = createColumnHelper<IConstituenta>();
interface EditorItemsProps { interface EditorItemsProps {
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
@ -25,7 +28,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
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 [toggledClearRows, setToggledClearRows] = useState(false); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
// Delete selected constituents // Delete selected constituents
function handleDelete() { function handleDelete() {
@ -33,8 +36,7 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
return; return;
} }
onDeleteCst(selected, () => { onDeleteCst(selected, () => {
setToggledClearRows(prev => !prev); setRowSelection({});
setSelected([]);
}); });
} }
@ -53,12 +55,16 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.max(0, currentIndex - 1) + 1 const target = Math.max(0, currentIndex - 1) + 1
const data = { const data = {
items: selected.map(id => { items: selected.map(id => ({ id: id })),
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data, () => {
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
})
setRowSelection(newSelection);
});
} }
// Move selected cst down // Move selected cst down
@ -80,12 +86,16 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
}, -1); }, -1);
const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1 const target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data: ICstMovetoData = { const data: ICstMovetoData = {
items: selected.map(id => { items: selected.map(id => ({ id: id })),
return { id: id };
}),
move_to: target move_to: target
} }
cstMoveTo(data); cstMoveTo(data, () => {
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
})
setRowSelection(newSelection);
});
} }
// Generate new names for all constituents // Generate new names for all constituents
@ -156,41 +166,53 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
const handleRowClicked = useCallback( const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => { (cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey) { if (event.altKey) {
event.preventDefault();
onOpenEdit(cst.id); onOpenEdit(cst.id);
} }
}, [onOpenEdit]); }, [onOpenEdit]);
const handleRowDoubleClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
event.preventDefault();
onOpenEdit(cst.id);
}, [onOpenEdit]);
const handleSelectionChange = useCallback( useLayoutEffect(
({ selectedRows }: { () => {
allSelected: boolean if (!schema || Object.keys(rowSelection).length === 0) {
selectedCount: number setSelected([]);
selectedRows: IConstituenta[] } else {
}) => { const selected: number[] = [];
setSelected(selectedRows.map(cst => cst.id)); schema.items.forEach((cst, index) => {
}, [setSelected]); if (rowSelection[String(index)] === true) {
selected.push(cst.id);
}
});
setSelected(selected);
}
}, [rowSelection, schema]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ columnHelper.accessor('alias', {
name: 'ID',
id: 'id',
selector: (cst: IConstituenta) => cst.id,
omit: true
},
{
name: 'Имя',
id: 'alias', id: 'alias',
cell: (cst: IConstituenta) => { header: 'Имя',
const info = mapStatusInfo.get(cst.status)!; size: 65,
minSize: 65,
cell: props => {
const cst = props.row.original;
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 px-1 text-center rounded-md whitespace-nowrap'
style={{borderWidth: "1px", style={{
borderColor: getCstStatusColor(cst.status, colors), borderWidth: "1px",
color: getCstStatusColor(cst.status, colors), borderColor: getCstStatusFgColor(cst.status, colors),
color: getCstStatusFgColor(cst.status, colors),
fontWeight: 600, fontWeight: 600,
backgroundColor: colors.bgDefault}} backgroundColor: colors.bgInput
}}
> >
{cst.alias} {cst.alias}
</div> </div>
@ -198,68 +220,64 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
anchorSelect={`#${prefixes.cst_list}${cst.alias}`} anchorSelect={`#${prefixes.cst_list}${cst.alias}`}
place='right' place='right'
> >
<p><b>Статус: </b> {info.tooltip}</p> <p><span className='font-semibold'>Статус</span>: {info!.tooltip}</p>
</ConceptTooltip> </ConceptTooltip>
</>); </>);
}, }
width: '65px', }),
maxWidth: '65px', columnHelper.accessor(cst => getCstTypificationLabel(cst), {
reorder: true,
},
{
name: 'Типизация',
id: 'type', id: 'type',
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getCstTypificationLabel(cst)}</div>, header: 'Типизация',
width: '175px', size: 175,
maxWidth: '175px', maxSize: 175,
wrap: true, cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
reorder: true, }),
hide: 1600 columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
},
{
name: 'Термин',
id: 'term', id: 'term',
selector: (cst: IConstituenta) => cst.term_resolved || cst.term_raw || '', header: 'Термин',
width: '350px', size: 350,
minWidth: '150px', minSize: 150,
maxWidth: '350px', maxSize: 350
wrap: true, }),
reorder: true columnHelper.accessor('definition_formal', {
},
{
name: 'Формальное определение',
id: 'expression', id: 'expression',
selector: (cst: IConstituenta) => cst.definition_formal || '', header: 'Формальное определение',
minWidth: '300px', size: 300,
maxWidth: '500px', minSize: 300,
grow: 2, maxSize: 500
wrap: true, }),
reorder: true columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
},
{
name: 'Текстовое определение',
id: 'definition', id: 'definition',
cell: (cst: IConstituenta) => ( header: 'Текстовое определение',
<div style={{ fontSize: 12 }}> size: 200,
{cst.definition_resolved || cst.definition_raw || ''} minSize: 200,
</div> cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
), }),
minWidth: '200px', columnHelper.accessor('convention', {
grow: 2,
wrap: true,
reorder: true
},
{
name: 'Конвенция / Комментарий',
id: 'convention', id: 'convention',
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{cst.convention ?? ''}</div>, header: 'Конвенция / Комментарий',
minWidth: '100px', minSize: 100,
wrap: true, maxSize: undefined,
reorder: true, cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
hide: 1800 }),
}
], [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
@ -330,30 +348,30 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
</ConceptTooltip> </ConceptTooltip>
</div> </div>
</div> </div>
<div className='w-full h-full' onKeyDown={handleTableKey}> <div className='w-full h-full text-sm' onKeyDown={handleTableKey}>
<ConceptDataTable <DataTable
data={schema?.items ?? []} data={schema?.items ?? []}
columns={columns} columns={columns}
keyField='id' state={{
rowSelection: rowSelection
}}
enableMultiRowSelection
onRowDoubleClicked={handleRowDoubleClicked}
onRowClicked={handleRowClicked}
onRowSelectionChange={setRowSelection}
noDataComponent={ noDataComponent={
<span className='flex flex-col justify-center p-2 text-center'> <span className='flex flex-col justify-center p-2 text-center'>
<p>Список пуст</p> <p>Список пуст</p>
<p>Создайте новую конституенту</p> <p
className='cursor-pointer text-primary hover:underline'
onClick={() => handleCreateCst()}>
Создать новую конституенту
</p>
</span> </span>
} }
striped
highlightOnHover
pointerOnHover
selectableRows
selectableRowsHighlight
selectableRowsComponentProps={{tabIndex: -1}}
onSelectedRowsChange={handleSelectionChange}
onRowDoubleClicked={cst => onOpenEdit(cst.id)}
onRowClicked={handleRowClicked}
clearSelectedRows={toggledClearRows}
dense
/> />
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@ import { graphDarkT, graphLightT, IColorTheme } from '../../utils/color';
import { prefixes, resources, TIMEOUT_GRAPH_REFRESH } from '../../utils/constants'; import { prefixes, resources, TIMEOUT_GRAPH_REFRESH } from '../../utils/constants';
import { Graph } from '../../utils/Graph'; import { Graph } from '../../utils/Graph';
import { CstType, IConstituenta, ICstCreateData } from '../../utils/models'; import { CstType, IConstituenta, ICstCreateData } from '../../utils/models';
import { getCstClassColor, getCstStatusColor, import { getCstClassColor, getCstStatusBgColor,
GraphColoringSelector, GraphLayoutSelector, GraphColoringSelector, GraphLayoutSelector,
mapColoringLabels, mapLayoutLabels mapColoringLabels, mapLayoutLabels
} from '../../utils/staticUI'; } from '../../utils/staticUI';
@ -34,7 +34,7 @@ function getCstNodeColor(cst: IConstituenta, coloringScheme: ColoringScheme, col
return getCstClassColor(cst.cst_class, colors); return getCstClassColor(cst.cst_class, colors);
} }
if (coloringScheme === 'status') { if (coloringScheme === 'status') {
return getCstStatusColor(cst.status, colors); return getCstStatusBgColor(cst.status, colors);
} }
return ''; return '';
} }

View File

@ -137,7 +137,7 @@ function RSTabs() {
if (element) { if (element) {
element.scrollIntoView({ element.scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'end', block: 'nearest',
inline: 'nearest' inline: 'nearest'
}); });
} }
@ -173,9 +173,7 @@ function RSTabs() {
return; return;
} }
const data = { const data = {
items: deleted.map(id => { items: deleted.map(id => ({ id: id }))
return { id: id };
})
}; };
let activeIndex = schema.items.findIndex(cst => cst.id === activeID); let activeIndex = schema.items.findIndex(cst => cst.id === activeID);
cstDelete(data, () => { cstDelete(data, () => {
@ -345,7 +343,7 @@ function RSTabs() {
showCloneDialog={handleShowClone} showCloneDialog={handleShowClone}
showUploadDialog={() => setShowUpload(true)} showUploadDialog={() => setShowUpload(true)}
/> />
<ConceptTab className='border-r-2 min-w-[7.8rem]'>Паспорт схемы</ConceptTab> <ConceptTab className='border-x-2 min-w-[7.8rem]'>Паспорт схемы</ConceptTab>
<ConceptTab className='border-r-2 min-w-[10rem] flex justify-between gap-2'> <ConceptTab className='border-r-2 min-w-[10rem] flex justify-between gap-2'>
<span>Конституенты</span> <span>Конституенты</span>
<span>{`${schema.stats?.count_errors ?? 0} | ${schema.stats?.count_all ?? 0}`}</span> <span>{`${schema.stats?.count_errors ?? 0} | ${schema.stats?.count_all ?? 0}`}</span>

View File

@ -71,9 +71,10 @@ function RSTabsMenu({
<div ref={schemaMenu.ref}> <div ref={schemaMenu.ref}>
<Button <Button
tooltip='Действия' tooltip='Действия'
icon={<MenuIcon size={5}/>} icon={<MenuIcon color='text-controls' size={5}/>}
borderClass='' borderClass=''
widthClass='h-full w-fit' widthClass='h-full w-fit'
style={{outlineColor: 'transparent'}}
dense dense
onClick={schemaMenu.toggle} onClick={schemaMenu.toggle}
tabIndex={-1} tabIndex={-1}
@ -123,6 +124,7 @@ function RSTabsMenu({
tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')} tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')}
borderClass='' borderClass=''
widthClass='h-full w-fit' widthClass='h-full w-fit'
style={{outlineColor: 'transparent'}}
icon={<PenIcon size={5} color={isEditable ? 'text-success' : 'text-warning'}/>} icon={<PenIcon size={5} color={isEditable ? 'text-success' : 'text-warning'}/>}
dense dense
onClick={editMenu.toggle} onClick={editMenu.toggle}
@ -136,7 +138,7 @@ function RSTabsMenu({
tooltip={!user || !isClaimable ? 'Стать владельцем можно только для общей изменяемой схемы' : ''} tooltip={!user || !isClaimable ? 'Стать владельцем можно только для общей изменяемой схемы' : ''}
> >
<div className='inline-flex items-center gap-1 justify-normal'> <div className='inline-flex items-center gap-1 justify-normal'>
<span className={isOwned ? 'text-success' : ''}><CrownIcon size={4} /></span> <span><CrownIcon size={4} color={isOwned ? 'text-success' : 'text-controls'} /></span>
<p> <p>
{ isOwned && <b>Владелец схемы</b> } { isOwned && <b>Владелец схемы</b> }
{ !isOwned && <b>Стать владельцем</b> } { !isOwned && <b>Стать владельцем</b> }
@ -165,10 +167,11 @@ function RSTabsMenu({
disabled={processing} disabled={processing}
icon={isTracking icon={isTracking
? <EyeIcon color='text-primary' size={5}/> ? <EyeIcon color='text-primary' size={5}/>
: <EyeOffIcon size={5}/> : <EyeOffIcon color='text-controls' size={5}/>
} }
widthClass='h-full w-fit' widthClass='h-full w-fit'
borderClass='' borderClass=''
style={{outlineColor: 'transparent'}}
dense dense
onClick={onToggleSubscribe} onClick={onToggleSubscribe}
tabIndex={-1} tabIndex={-1}

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useConceptTheme } from '../../../context/ThemeContext'; import { useConceptTheme } from '../../../context/ThemeContext';
import { ExpressionStatus, type IConstituenta, IExpressionParse,inferStatus, ParsingStatus } from '../../../utils/models'; import { ExpressionStatus, type IConstituenta, IExpressionParse,inferStatus, ParsingStatus } from '../../../utils/models';
import { getCstStatusColor, mapStatusInfo } from '../../../utils/staticUI'; import { getCstStatusBgColor, mapStatusInfo } from '../../../utils/staticUI';
interface StatusBarProps { interface StatusBarProps {
isModified?: boolean isModified?: boolean
@ -27,7 +27,7 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
return ( return (
<div title={data.tooltip} <div title={data.tooltip}
className='text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle' className='text-sm h-[1.6rem] w-[10rem] font-semibold inline-flex border items-center select-none justify-center align-middle'
style={{backgroundColor: getCstStatusColor(status, colors)}} style={{backgroundColor: getCstStatusBgColor(status, colors)}}
> >
Статус: [ {data.text} ] Статус: [ {data.text} ]
</div> </div>

View File

@ -1,12 +1,13 @@
import { createColumnHelper } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import ConceptDataTable from '../../../components/Common/ConceptDataTable'; import DataTable from '../../../components/Common/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 { 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, getCstStatusColor, getMockConstituenta } from '../../../utils/staticUI'; import { getCstDescription, getCstStatusFgColor, getMockConstituenta } from '../../../utils/staticUI';
import ConstituentaTooltip from './ConstituentaTooltip'; import ConstituentaTooltip from './ConstituentaTooltip';
import DependencyModePicker from './DependencyModePicker'; import DependencyModePicker from './DependencyModePicker';
import MatchModePicker from './MatchModePicker'; import MatchModePicker from './MatchModePicker';
@ -25,6 +26,8 @@ function isMockCst(cst: IConstituenta) {
return cst.id <= 0; return cst.id <= 0;
} }
const columnHelper = createColumnHelper<IConstituenta>();
function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) { function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }: ViewSideConstituentsProps) {
const { noNavigation, colors } = useConceptTheme(); const { noNavigation, colors } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
@ -85,75 +88,61 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
} }
}, [onOpenEdit]); }, [onOpenEdit]);
const conditionalRowStyles = useMemo(
() => [
{
when: (cst: IConstituenta) => cst.id === activeID,
style: {
backgroundColor: colors.bgSelected,
},
}
], [activeID, colors]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ columnHelper.accessor('alias', {
id: 'id',
selector: (cst: IConstituenta) => cst.id,
omit: true
},
{
name: 'ID',
id: 'alias', id: 'alias',
cell: (cst: IConstituenta) => { header: 'Имя',
size: 65,
minSize: 65,
cell: props => {
const cst = props.row.original;
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 min-w-fit whitespace-nowrap' className='w-full px-1 text-center rounded-md whitespace-nowrap'
style={{backgroundColor: getCstStatusColor(cst.status, colors)}} style={{
borderWidth: '1px',
borderColor: getCstStatusFgColor(cst.status, colors),
color: getCstStatusFgColor(cst.status, colors),
fontWeight: 600,
backgroundColor: isMockCst(cst) ? colors.bgWarning : colors.bgInput
}}
> >
{cst.alias} {cst.alias}
</div> </div>
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} /> <ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_list}${cst.alias}`} />
</>); </>);
}, }
width: '65px', }),
maxWidth: '65px', columnHelper.accessor(cst => getCstDescription(cst), {
conditionalCellStyles: [
{
when: (cst: IConstituenta) => isMockCst(cst),
style: {backgroundColor: colors.bgWarning}
}
]
},
{
name: 'Описание',
id: 'description', id: 'description',
selector: (cst: IConstituenta) => getCstDescription(cst), header: 'Описание',
minWidth: '350px', size: 350,
wrap: true, minSize: 350,
conditionalCellStyles: [ maxSize: 350,
{ cell: props =>
when: (cst: IConstituenta) => isMockCst(cst), <div style={{
style: {backgroundColor: colors.bgWarning} fontSize: 12,
} color: isMockCst(props.row.original) ? colors.fgWarning : undefined
] }}>
}, {props.getValue()}
{ </div>
name: 'Выражение', }),
columnHelper.accessor('definition_formal', {
id: 'expression', id: 'expression',
selector: (cst: IConstituenta) => cst.definition_formal || '', header: 'Выражение',
minWidth: '200px', size: 700,
hide: 1600, minSize: 0,
grow: 2, maxSize: 700,
wrap: true, cell: props =>
conditionalCellStyles: [ <div style={{
{ fontSize: 12,
when: (cst: IConstituenta) => isMockCst(cst), color: isMockCst(props.row.original) ? colors.fgWarning : undefined
style: {backgroundColor: colors.fgWarning} }}>
} {props.getValue()}
] </div>
} })
], [colors]); ], [colors]);
const maxHeight = useMemo( const maxHeight = useMemo(
@ -181,12 +170,14 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
onChange={setFilterSource} onChange={setFilterSource}
/> />
</div> </div>
<div className='overflow-y-auto' style={{maxHeight : `${maxHeight}`}}> <div className='overflow-y-auto text-sm' style={{maxHeight : `${maxHeight}`}}>
<ConceptDataTable <DataTable
data={filteredData} data={filteredData}
columns={columns} columns={columns}
keyField='id'
conditionalRowStyles={conditionalRowStyles}
// conditionalRowStyles={conditionalRowStyles}
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]'>
<p>Список конституент пуст</p> <p>Список конституент пуст</p>
@ -194,13 +185,8 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
</span> </span>
} }
striped
highlightOnHover
pointerOnHover
onRowDoubleClicked={handleDoubleClick} onRowDoubleClicked={handleDoubleClick}
onRowClicked={handleRowClicked} onRowClicked={handleRowClicked}
dense
/> />
</div> </div>
</>); </>);

View File

@ -1,7 +1,8 @@
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 ConceptDataTable from '../../components/Common/ConceptDataTable'; import DataTable from '../../components/Common/DataTable';
import { useConceptNavigation } from '../../context/NagivationContext'; import { useConceptNavigation } from '../../context/NagivationContext';
import { ILibraryItem } from '../../utils/models'; import { ILibraryItem } from '../../utils/models';
@ -9,6 +10,8 @@ interface ViewSubscriptionsProps {
items: ILibraryItem[] items: ILibraryItem[]
} }
const columnHelper = createColumnHelper<ILibraryItem>();
function ViewSubscriptions({items}: ViewSubscriptionsProps) { function ViewSubscriptions({items}: ViewSubscriptionsProps) {
const { navigateTo } = useConceptNavigation(); const { navigateTo } = useConceptNavigation();
const intl = useIntl(); const intl = useIntl();
@ -17,50 +20,48 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
const columns = useMemo(() => const columns = useMemo(() =>
[ [
{ columnHelper.accessor('alias', {
name: 'Шифр',
id: 'alias', id: 'alias',
maxWidth: '140px', header: 'Шифр',
selector: (item: ILibraryItem) => item.alias, size: 200,
sortable: true, minSize: 200,
reorder: true maxSize: 200,
}, enableSorting: true
{ }),
name: 'Название', columnHelper.accessor('title', {
id: 'title', id: 'title',
minWidth: '50%', header: 'Название',
selector: (item: ILibraryItem) => item.title, minSize: 200,
sortable: true, size: 800,
reorder: true maxSize: 800,
}, enableSorting: true
{ }),
name: 'Обновлена', columnHelper.accessor('time_update', {
id: 'time_update', id: 'time_update',
selector: (item: ILibraryItem) => item.time_update, header: 'Обновлена',
format: (item: ILibraryItem) => new Date(item.time_update).toLocaleString(intl.locale), minSize: 200,
sortable: true, size: 200,
reorder: true maxSize: 200,
} cell: props => new Date(props.cell.getValue()).toLocaleString(intl.locale),
enableSorting: true
})
], [intl]); ], [intl]);
return ( return (
<ConceptDataTable <div className='h-full overflow-auto text-sm border w-fit'>
className='h-full overflow-auto border' <DataTable
columns={columns} columns={columns}
data={items} data={items}
defaultSortFieldId='time_update' // defaultSortFieldId='time_update'
defaultSortAsc={false} // defaultSortAsc={false}
noDataComponent={ noDataComponent={
<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div> <div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>
} }
striped
dense
highlightOnHover
pointerOnHover
onRowClicked={openRSForm} onRowClicked={openRSForm}
/> />
</div>
) )
} }

View File

@ -95,67 +95,14 @@ export const darkT: IColorTheme = {
bgTeal: 'hsl(192, 080%, 030%)', bgTeal: 'hsl(192, 080%, 030%)',
bgOrange: 'hsl(035, 100%, 035%)', bgOrange: 'hsl(035, 100%, 035%)',
fgRed: 'hsl(000, 080%, 050%)', fgRed: 'hsl(000, 080%, 045%)',
fgGreen: 'hsl(100, 080%, 040%)', fgGreen: 'hsl(100, 080%, 035%)',
fgBlue: 'hsl(235, 100%, 080%)', fgBlue: 'hsl(235, 100%, 080%)',
fgPurple: 'hsl(270, 100%, 080%)', fgPurple: 'hsl(270, 100%, 080%)',
fgTeal: 'hsl(192, 100%, 030%)', fgTeal: 'hsl(192, 100%, 030%)',
fgOrange: 'hsl(035, 100%, 050%)' fgOrange: 'hsl(035, 100%, 050%)'
}; };
// ========= DATA TABLE THEMES ========
export const dataTableLightT = {
text: {
primary: lightT.fgDefault,
secondary: lightT.fgDefault,
disabled: lightT.fgDisabled
},
background: {
default: lightT.bgDefault
},
highlightOnHover: {
default: lightT.bgHover,
text: lightT.fgDefault
},
divider: {
default: lightT.border
},
striped: {
default: lightT.bgControls,
text: lightT.fgDefault
},
selected: {
default: lightT.bgSelected,
text: lightT.fgDefault
}
}
export const dataTableDarkT = {
text: {
primary: darkT.fgDefault,
secondary: darkT.fgDefault,
disabled: darkT.fgDisabled
},
background: {
default: darkT.bgDefault
},
highlightOnHover: {
default: darkT.bgHover,
text: darkT.fgDefault
},
divider: {
default: darkT.border
},
striped: {
default: darkT.bgControls,
text: darkT.fgDefault
},
selected: {
default: darkT.bgSelected,
text: darkT.fgDefault
}
};
// ============ SELECT THEMES ========== // ============ SELECT THEMES ==========
export const selectLightT = { export const selectLightT = {
primary: lightT.bgPrimary, primary: lightT.bgPrimary,

View File

@ -242,11 +242,13 @@ export function getCstTypeShortcut(type: CstType) {
} }
} }
export const CstTypeSelector = (Object.values(CstType)).map( export const CstTypeSelector = (
(typeStr) => { Object.values(CstType)).map(
const type = typeStr as CstType; typeStr => ({
return { value: type, label: getCstTypeLabel(type) }; value: typeStr as CstType,
}); label: getCstTypeLabel(typeStr as CstType)
})
);
export function getCstCompareLabel(mode: CstMatchMode): string { export function getCstCompareLabel(mode: CstMatchMode): string {
switch(mode) { switch(mode) {
@ -313,7 +315,18 @@ export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [
{ value: 'type', label: 'Цвет: класс'}, { value: 'type', label: 'Цвет: класс'},
]; ];
export function getCstStatusColor(status: ExpressionStatus, colors: IColorTheme): string { export function getCstStatusBgColor(status: ExpressionStatus, colors: IColorTheme): string {
switch (status) {
case ExpressionStatus.VERIFIED: return colors.bgGreen;
case ExpressionStatus.INCORRECT: return colors.bgRed;
case ExpressionStatus.INCALCULABLE: return colors.bgOrange;
case ExpressionStatus.PROPERTY: return colors.bgTeal;
case ExpressionStatus.UNKNOWN: return colors.bgBlue;
case ExpressionStatus.UNDEFINED: return colors.bgBlue;
}
}
export function getCstStatusFgColor(status: ExpressionStatus, colors: IColorTheme): string {
switch (status) { switch (status) {
case ExpressionStatus.VERIFIED: return colors.fgGreen; case ExpressionStatus.VERIFIED: return colors.fgGreen;
case ExpressionStatus.INCORRECT: return colors.fgRed; case ExpressionStatus.INCORRECT: return colors.fgRed;