Refactoring: replace shortcurctuit with conditionals

This commit is contained in:
IRBorisov 2023-11-27 11:33:34 +03:00
parent 747176c673
commit de9c470276
72 changed files with 906 additions and 910 deletions

View File

@ -20,32 +20,33 @@ import { globalIDs } from './utils/constants';
function Root() { function Root() {
const { noNavigation, noFooter, viewportHeight, mainHeight, showScroll } = useConceptTheme(); const { noNavigation, noFooter, viewportHeight, mainHeight, showScroll } = useConceptTheme();
return ( return (
<NavigationState> <NavigationState>
<div className='w-screen antialiased clr-app min-w-[30rem] overflow-hidden'> <div className='w-screen antialiased clr-app min-w-[30rem] overflow-hidden'>
<ConceptToaster
className='mt-[4rem] text-sm'
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
/>
<Navigation />
<div id={globalIDs.main_scroll}
className='w-full overflow-x-auto overscroll-none'
style={{
maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto'
}}
>
<main className='w-full h-full min-w-fit' style={{minHeight: mainHeight}}>
<Outlet />
</main>
<ConceptToaster {(!noNavigation && !noFooter) ? <Footer /> : null}
className='mt-[4rem] text-sm'
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
/>
<Navigation />
<div id={globalIDs.main_scroll}
className='w-full overflow-x-auto overscroll-none'
style={{
maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto'
}}
>
<main className='w-full h-full min-w-fit' style={{minHeight: mainHeight}}>
<Outlet />
</main>
{!noNavigation && !noFooter && <Footer />}
</div>
</div> </div>
</NavigationState> </div>
); </NavigationState>);
} }
const router = createBrowserRouter([ const router = createBrowserRouter([

View File

@ -50,8 +50,8 @@ function DescribeError(error: ErrorInfo) {
<p>{error.message}</p> <p>{error.message}</p>
{error.response.data && (<> {error.response.data && (<>
<p className='mt-2 underline'>Описание</p> <p className='mt-2 underline'>Описание</p>
{ isHtml && <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> } {isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
{ !isHtml && <PrettyJson data={error.response.data as object} />} {!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
</>)} </>)}
</div> </div>
); );

View File

@ -28,8 +28,8 @@ function Button({
className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${colors} ${outlineClass} ${borderClass} ${dimensions} ${cursor}`} className={`inline-flex items-center gap-2 align-middle justify-center select-none ${padding} ${colors} ${outlineClass} ${borderClass} ${dimensions} ${cursor}`}
{...props} {...props}
> >
{icon && icon} {icon ? icon : null}
{text && <span className='font-semibold'>{text}</span>} {text ? <span className='font-semibold'>{text}</span> : null}
</button> </button>
); );
} }

View File

@ -5,7 +5,6 @@ import Label from './Label';
export interface CheckboxProps export interface CheckboxProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'value' | 'onClick' > { extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'value' | 'onClick' > {
id?: string
label?: string label?: string
disabled?: boolean disabled?: boolean
dimensions?: string dimensions?: string
@ -43,8 +42,7 @@ function Checkbox({
} }
return ( return (
<button <button type='button' id={id}
id={id}
className={`flex items-center clr-outline focus:outline-dotted focus:outline-1 ${dimensions}`} className={`flex items-center clr-outline focus:outline-dotted focus:outline-1 ${dimensions}`}
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}
@ -52,14 +50,17 @@ 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]'><CheckboxCheckedIcon /></div>} {value ?
<div className='mt-[1px] ml-[1px]'>
<CheckboxCheckedIcon />
</div> : null}
</div> </div>
{ label && {label ?
<Label <Label
className={`${cursor} px-2 text-start`} className={`${cursor} px-2 text-start`}
text={label} text={label}
htmlFor={id} htmlFor={id}
/>} /> : null}
</button> </button>
); );
} }

View File

@ -20,7 +20,7 @@ function ConceptSearch({ value, onChange, dense }: ConceptSearchProps) {
noOutline noOutline
noBorder={dense} noBorder={dense}
value={value} value={value}
onChange={event => onChange && onChange(event.target.value)} onChange={event => (onChange ? onChange(event.target.value) : undefined)}
/> />
</div>); </div>);
} }

View File

@ -6,10 +6,10 @@ interface DropdownButtonProps {
} }
function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) { function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButtonProps) {
const behavior = (onClick ? 'cursor-pointer disabled:cursor-not-allowed clr-hover' : 'cursor-default'); const behavior = onClick ? 'cursor-pointer disabled:cursor-not-allowed clr-hover' : 'cursor-default';
const text = disabled ? 'text-controls' : ''; const text = disabled ? 'text-controls' : '';
return ( return (
<button <button type='button'
disabled={disabled} disabled={disabled}
title={tooltip} title={tooltip}
onClick={onClick} onClick={onClick}

View File

@ -9,7 +9,7 @@ interface DropdownCheckboxProps {
} }
function DropdownCheckbox({ tooltip, setValue, disabled, ...props }: DropdownCheckboxProps) { function DropdownCheckbox({ tooltip, setValue, disabled, ...props }: DropdownCheckboxProps) {
const behavior = (setValue && !disabled ? 'clr-hover' : ''); const behavior = (setValue && !disabled) ? 'clr-hover' : '';
return ( return (
<div <div
title={tooltip} title={tooltip}

View File

@ -11,7 +11,7 @@ function Form({ title, onSubmit, dimensions = 'max-w-xs', children }: FormProps)
className={`border shadow-md py-2 clr-app px-6 flex flex-col gap-3 ${dimensions}`} className={`border shadow-md py-2 clr-app px-6 flex flex-col gap-3 ${dimensions}`}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{ title && <h1 className='text-xl whitespace-nowrap'>{title}</h1> } { title ? <h1 className='text-xl whitespace-nowrap'>{title}</h1> : null }
{children} {children}
</form> </form>
); );

View File

@ -38,25 +38,23 @@ function Modal({
return ( return (
<> <>
<div className='fixed top-0 left-0 w-full h-full z-navigation clr-modal-backdrop' /> <div className='fixed top-0 left-0 w-full h-full z-navigation clr-modal-backdrop' />
<div <div ref={ref}
ref={ref} className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-4 py-3 flex flex-col justify-start w-fit max-w-[calc(100vw-2rem)] overflow-x-auto h-fit z-modal clr-app border shadow-md'
className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-6 py-3 flex flex-col justify-start w-fit max-w-[calc(100vw-2rem)] overflow-x-auto h-fit z-modal clr-app border shadow-md'
> >
{ title && <h1 className='pb-3 text-xl select-none'>{title}</h1> } {title ? <h1 className='pb-3 text-xl select-none'>{title}</h1> : null}
<div className='max-h-[calc(100vh-8rem)] overflow-auto'> <div className='max-h-[calc(100vh-8rem)] overflow-auto px-2'>
{children} {children}
</div> </div>
<div className='flex justify-center w-full gap-4 pt-3 mt-2 border-t-2 z-modal-controls'> <div className='flex justify-center w-full gap-4 pt-3 mt-2 border-t-2 z-modal-controls'>
{!readonly && {!readonly ?
<Button <Button autoFocus
text={submitText} text={submitText}
tooltip={!canSubmit ? submitInvalidTooltip: ''} tooltip={!canSubmit ? submitInvalidTooltip: ''}
dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit' dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit'
colors='clr-btn-primary' colors='clr-btn-primary'
disabled={!canSubmit} disabled={!canSubmit}
onClick={handleSubmit} onClick={handleSubmit}
autoFocus /> : null}
/>}
<Button <Button
text={readonly ? 'Закрыть' : 'Отмена'} text={readonly ? 'Закрыть' : 'Отмена'}
dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit' dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit'

View File

@ -18,15 +18,16 @@ function SelectorButton({
...props ...props
}: SelectorButtonProps) { }: SelectorButtonProps) {
const cursor = 'disabled:cursor-not-allowed cursor-pointer'; const cursor = 'disabled:cursor-not-allowed cursor-pointer';
const position = `px-1 flex flex-start items-center gap-1 ${dimensions}` const position = `px-1 flex flex-start items-center gap-1 ${dimensions}`;
const design = (transparent ? 'clr-hover' : `border ${colors}`) + ' text-btn text-controls';
return ( return (
<button type='button' <button type='button'
className={`text-sm small-caps ${!transparent && 'border'} ${cursor} ${position} text-btn text-controls select-none ${transparent ? 'clr-hover' : colors}`} className={`text-sm small-caps select-none ${cursor} ${position} ${design}`}
title={tooltip} title={tooltip}
{...props} {...props}
> >
{icon && icon} {icon ? icon : null}
{text && <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div>} {text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
</button> </button>
); );
} }

View File

@ -17,8 +17,8 @@ function SubmitButton({
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${dimensions} ${loading ? ' cursor-progress' : ''}`} className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${dimensions} ${loading ? ' cursor-progress' : ''}`}
disabled={disabled ?? loading} disabled={disabled ?? loading}
> >
{icon && <span>{icon}</span>} {icon ? <span>{icon}</span> : null}
{text && <span>{text}</span>} {text ? <span>{text}</span> : null}
</button>); </button>);
} }

View File

@ -22,7 +22,7 @@ function SwitchButton<ValueType>({
className={`px-2 py-1 border font-semibold small-caps rounded-none cursor-pointer clr-btn-clear clr-hover ${dimensions} ${isSelected ? 'clr-selected': ''}`} className={`px-2 py-1 border font-semibold small-caps rounded-none cursor-pointer clr-btn-clear clr-hover ${dimensions} ${isSelected ? 'clr-selected': ''}`}
{...props} {...props}
> >
{icon && icon} {icon ? icon : null}
{label} {label}
</button>); </button>);
} }

View File

@ -23,15 +23,14 @@ function TextInput({
const outlineClass = noOutline ? '' : 'clr-outline'; const outlineClass = noOutline ? '' : 'clr-outline';
return ( return (
<div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}> <div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}>
{label && {label ?
<Label <Label
text={label} text={label}
htmlFor={id} htmlFor={id}
/>} /> : null}
<input id={id} <input id={id}
title={tooltip} title={tooltip}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture: onKeyDown}
className={`px-3 py-2 leading-tight truncate hover:text-clip ${colors} ${outlineClass} ${borderClass} ${dense ? 'w-full' : dimensions}`} className={`px-3 py-2 leading-tight truncate hover:text-clip ${colors} ${outlineClass} ${borderClass} ${dense ? 'w-full' : dimensions}`}
{...props} {...props}
/> />

View File

@ -45,8 +45,7 @@ function Tristate({
} }
return ( return (
<button <button type='button' id={id}
id={id}
className={`flex items-center clr-outline focus:outline-dotted focus:outline-1 ${dimensions}`} className={`flex items-center clr-outline focus:outline-dotted focus:outline-1 ${dimensions}`}
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}
@ -54,15 +53,15 @@ 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]'><CheckboxCheckedIcon /></div>} {value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null}
{ value == null && <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div>} {value == null ? <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div> : null}
</div> </div>
{ label && {label ?
<Label <Label
className={`${cursor} px-2 text-start`} className={`${cursor} px-2 text-start`}
text={label} text={label}
htmlFor={id} htmlFor={id}
/>} /> : null}
</button> </button>
); );
} }

View File

@ -113,7 +113,7 @@ export default function DataTable<TData extends RowData>({
<div className='w-full'> <div className='w-full'>
<div className='flex flex-col items-stretch'> <div className='flex flex-col items-stretch'>
<table> <table>
{ !noHeader && {!noHeader ?
<thead <thead
className={`clr-app shadow-border`} className={`clr-app shadow-border`}
style={{ style={{
@ -124,10 +124,10 @@ export default function DataTable<TData extends RowData>({
{tableImpl.getHeaderGroups().map( {tableImpl.getHeaderGroups().map(
(headerGroup: HeaderGroup<TData>) => ( (headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}> <tr key={headerGroup.id}>
{enableRowSelection && {enableRowSelection ?
<th className='pl-3 pr-1'> <th className='pl-3 pr-1'>
<SelectAll table={tableImpl} /> <SelectAll table={tableImpl} />
</th>} </th> : null}
{headerGroup.headers.map( {headerGroup.headers.map(
(header: Header<TData, unknown>) => ( (header: Header<TData, unknown>) => (
<th key={header.id} <th key={header.id}
@ -140,16 +140,16 @@ export default function DataTable<TData extends RowData>({
}} }}
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined} onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
> >
{header.isPlaceholder ? null : ( {!header.isPlaceholder ? (
<div className='flex gap-1'> <div className='flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
{enableSorting && header.column.getCanSort() && <SortingIcon column={header.column} />} {(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null}
</div>)} </div>) : null}
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</thead>} </thead> : null}
<tbody> <tbody>
{tableImpl.getRowModel().rows.map( {tableImpl.getRowModel().rows.map(
@ -162,10 +162,10 @@ export default function DataTable<TData extends RowData>({
} }
style={conditionalRowStyles && getRowStyles(row)} style={conditionalRowStyles && getRowStyles(row)}
> >
{enableRowSelection && {enableRowSelection ?
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'> <td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
<SelectRow row={row} /> <SelectRow row={row} />
</td>} </td> : null}
{row.getVisibleCells().map( {row.getVisibleCells().map(
(cell: Cell<TData, unknown>) => ( (cell: Cell<TData, unknown>) => (
<td <td
@ -176,8 +176,8 @@ export default function DataTable<TData extends RowData>({
paddingBottom: dense ? '0.25rem': '0.5rem', paddingBottom: dense ? '0.25rem': '0.5rem',
paddingTop: dense ? '0.25rem': '0.5rem' paddingTop: dense ? '0.25rem': '0.5rem'
}} }}
onClick={event => onRowClicked && onRowClicked(row.original, event)} onClick={event => onRowClicked ? onRowClicked(row.original, event) : undefined}
onDoubleClick={event => onRowDoubleClicked && onRowDoubleClicked(row.original, event)} onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </td>
@ -186,7 +186,7 @@ export default function DataTable<TData extends RowData>({
))} ))}
</tbody> </tbody>
{!noFooter && {!noFooter ?
<tfoot> <tfoot>
{tableImpl.getFooterGroups().map( {tableImpl.getFooterGroups().map(
(footerGroup: HeaderGroup<TData>) => ( (footerGroup: HeaderGroup<TData>) => (
@ -194,23 +194,21 @@ export default function DataTable<TData extends RowData>({
{footerGroup.headers.map( {footerGroup.headers.map(
(header: Header<TData, unknown>) => ( (header: Header<TData, unknown>) => (
<th key={header.id}> <th key={header.id}>
{header.isPlaceholder ? null {!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
: flexRender(header.column.columnDef.footer, header.getContext())
}
</th> </th>
))} ))}
</tr> </tr>
))} ))}
</tfoot>} </tfoot> : null}
</table> </table>
{enablePagination && !isEmpty && {(enablePagination && !isEmpty) ?
<PaginationTools <PaginationTools
table={tableImpl} table={tableImpl}
paginationOptions={paginationOptions} paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption} onChangePaginationOption={onChangePaginationOption}
/>} /> : null}
</div> </div>
{isEmpty && (noDataComponent ?? <DefaultNoData />)} {isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
</div>); </div>);
} }

View File

@ -1,6 +1,7 @@
import { Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { prefixes } from '../../utils/constants';
import { GotoFirstIcon, GotoLastIcon, GotoNextIcon, GotoPrevIcon } from '../Icons'; import { GotoFirstIcon, GotoLastIcon, GotoNextIcon, GotoPrevIcon } from '../Icons';
interface PaginationToolsProps<TData> { interface PaginationToolsProps<TData> {
@ -20,7 +21,7 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
}, [onChangePaginationOption, table]); }, [onChangePaginationOption, table]);
return ( return (
<div className='text-sm flex justify-end w-full items-center text-controls select-none my-2'> <div className='flex items-center justify-end w-full my-2 text-sm select-none text-controls'>
<div className='flex items-center gap-1 mr-3'> <div className='flex items-center gap-1 mr-3'>
<div className=''> <div className=''>
{table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
@ -31,14 +32,14 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
<div className=''>{table.getFilteredRowModel().rows.length}</div> <div className=''>{table.getFilteredRowModel().rows.length}</div>
</div> </div>
<div className='flex'> <div className='flex'>
<button <button type='button'
className='clr-hover text-controls' className='clr-hover text-controls'
onClick={() => table.setPageIndex(0)} onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<GotoFirstIcon /> <GotoFirstIcon />
</button> </button>
<button <button type='button'
className='clr-hover text-controls' className='clr-hover text-controls'
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
@ -47,7 +48,7 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
</button> </button>
<input type='text' <input type='text'
title='Номер страницы. Выделите для ручного ввода' title='Номер страницы. Выделите для ручного ввода'
className='w-6 clr-app text-center' className='w-6 text-center clr-app'
value={table.getState().pagination.pageIndex + 1} value={table.getState().pagination.pageIndex + 1}
onChange={event => { onChange={event => {
const page = event.target.value ? Number(event.target.value) - 1 : 0; const page = event.target.value ? Number(event.target.value) - 1 : 0;
@ -56,14 +57,15 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
} }
}} }}
/> />
<button <button type='button'
className='clr-hover text-controls' className='clr-hover text-controls'
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<GotoNextIcon /> <GotoNextIcon />
</button> </button>
<button className='clr-hover text-controls' <button type='button'
className='clr-hover text-controls'
onClick={() => table.setPageIndex(table.getPageCount() - 1)} onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
@ -73,11 +75,11 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
<select <select
value={table.getState().pagination.pageSize} value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange} onChange={handlePaginationOptionsChange}
className='clr-app mx-2 cursor-pointer' className='mx-2 cursor-pointer clr-app'
> >
{paginationOptions.map( {paginationOptions.map(
pageSize => ( (pageSize) => (
<option key={pageSize} value={pageSize}> <option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
{pageSize} на стр {pageSize} на стр
</option> </option>
))} ))}

View File

@ -8,16 +8,15 @@ interface SelectAllProps<TData> {
function SelectAll<TData>({ table }: SelectAllProps<TData>) { function SelectAll<TData>({ table }: SelectAllProps<TData>) {
return ( return (
<Tristate <Tristate tabIndex={-1}
tabIndex={-1} tooltip='Выделить все'
value={ value={
!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : (!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected())
table.getIsAllPageRowsSelected() ? null
} : table.getIsAllPageRowsSelected()
tooltip='Выделить все' }
setValue={value => table.toggleAllPageRowsSelected(value !== false)} setValue={value => table.toggleAllPageRowsSelected(value !== false)}
/> />);
);
} }
export default SelectAll; export default SelectAll;

View File

@ -8,12 +8,10 @@ interface SelectRowProps<TData> {
function SelectRow<TData>({ row }: SelectRowProps<TData>) { function SelectRow<TData>({ row }: SelectRowProps<TData>) {
return ( return (
<Checkbox <Checkbox tabIndex={-1}
tabIndex={-1} value={row.getIsSelected()}
value={row.getIsSelected()} setValue={row.getToggleSelectedHandler()}
setValue={row.getToggleSelectedHandler()} />);
/>
);
} }
export default SelectRow; export default SelectRow;

View File

@ -12,7 +12,8 @@ function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
desc: <DescendingIcon size={4} />, desc: <DescendingIcon size={4} />,
asc: <AscendingIcon size={4}/>, asc: <AscendingIcon size={4}/>,
}[column.getIsSorted() as string] ?? }[column.getIsSorted() as string] ??
<DescendingIcon size={4} color='opacity-0 hover:opacity-50' />} <DescendingIcon size={4} color='opacity-0 hover:opacity-50' />
}
</>); </>);
} }

View File

@ -7,7 +7,7 @@ function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return ( return (
<div className='flex flex-col items-center antialiased clr-app' role='alert'> <div className='flex flex-col items-center antialiased clr-app' role='alert'>
<h1 className='text-lg font-semibold'>Что-то пошло не так!</h1> <h1 className='text-lg font-semibold'>Что-то пошло не так!</h1>
{ error } {error}
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' /> <Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
</div> </div>
); );

View File

@ -12,8 +12,7 @@ function Footer() {
<Link className='mx-2 hover:underline' to={urls.concept} tabIndex={-1}>Центр Концепт</Link> <Link className='mx-2 hover:underline' to={urls.concept} tabIndex={-1}>Центр Концепт</Link>
<Link className='mx-2 hover:underline' to='/manuals?topic=exteor' tabIndex={-1}>Экстеор</Link> <Link className='mx-2 hover:underline' to='/manuals?topic=exteor' tabIndex={-1}>Экстеор</Link>
</div> </div>
<div className=''> <div>
<p className='mt-0.5 text-center'>© 2023 ЦИВТ КОНЦЕПТ</p> <p className='mt-0.5 text-center'>© 2023 ЦИВТ КОНЦЕПТ</p>
</div> </div>

View File

@ -8,15 +8,32 @@ extends React.HTMLAttributes<HTMLDivElement> {
function InfoConstituenta({ data, ...props }: InfoConstituentaProps) { function InfoConstituenta({ data, ...props }: InfoConstituentaProps) {
return ( return (
<div {...props}> <div {...props}>
<h1>Конституента {data.alias}</h1> <h1>Конституента {data.alias}</h1>
<p><b>Типизация: </b>{labelCstTypification(data)}</p> <p>
<p><b>Термин: </b>{data.term_resolved || data.term_raw}</p> <b>Типизация: </b>
{data.definition_formal && <p><b>Выражение: </b>{data.definition_formal}</p>} {labelCstTypification(data)}
{data.definition_resolved && <p><b>Определение: </b>{data.definition_resolved}</p>} </p>
{data.convention && <p><b>Конвенция: </b>{data.convention}</p>} <p>
</div> <b>Термин: </b>
); {data.term_resolved || data.term_raw}
</p>
{data.definition_formal ?
<p>
<b>Выражение: </b>
{data.definition_formal}
</p> : null}
{data.definition_resolved ?
<p>
<b>Определение: </b>
{data.definition_resolved}
</p> : null}
{data.convention ?
<p>
<b>Конвенция: </b>
{data.convention}
</p> : null}
</div>);
} }
export default InfoConstituenta; export default InfoConstituenta;

View File

@ -12,26 +12,25 @@ function InfoCstClass({ title }: InfoCstClassProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1 mb-2'> <div className='flex flex-col gap-1 mb-2'>
{ title && <h1>{title}</h1>} {title ? <h1>{title}</h1> : null}
{ Object.values(CstClass).map( {Object.values(CstClass).map(
(cclass, index) => { (cclass, index) => {
return ( return (
<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-[7rem] text-center border text-sm small-caps' className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm small-caps'
style={{backgroundColor: colorbgCstClass(cclass, colors)}} style={{backgroundColor: colorbgCstClass(cclass, colors)}}
> >
{labelCstClass(cclass)} {labelCstClass(cclass)}
</span> </span>
<span> - </span> <span> - </span>
<span> <span>
{describeCstClass(cclass)} {describeCstClass(cclass)}
</span> </span>
</p>); </p>);
})} })}
</div> </div>);
);
} }
export default InfoCstClass; export default InfoCstClass;

View File

@ -12,29 +12,28 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1 h-fit mb-2'> <div className='flex flex-col gap-1 mb-2 h-fit'>
{ title && <h1>{title}</h1>} {title ? <h1>{title}</h1> : null}
{ Object.values(ExpressionStatus) {Object.values(ExpressionStatus)
.filter(status => status !== ExpressionStatus.UNDEFINED) .filter(status => status !== ExpressionStatus.UNDEFINED)
.map( .map(
(status, index) => { (status, index) => {
return ( return (
<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-[7rem] text-center border text-sm small-caps' className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm small-caps'
style={{backgroundColor: colorbgCstStatus(status, colors)}} style={{backgroundColor: colorbgCstStatus(status, colors)}}
> >
{labelExpressionStatus(status)} {labelExpressionStatus(status)}
</span> </span>
<span> - </span> <span> - </span>
<span> <span>
{describeExpressionStatus(status)} {describeExpressionStatus(status)}
</span> </span>
</p>); </p>);
} }
)} )}
</div> </div>);
);
} }
export default InfoCstStatus; export default InfoCstStatus;

View File

@ -10,23 +10,25 @@ function Logo() {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
return ( return (
<Link to='/' className='flex items-center h-full mr-2' tabIndex={-1}> <Link to='/' tabIndex={-1}
{ (windowSize.width && windowSize.width >= HIDE_LOGO_TEXT_LIMIT) && !darkMode && className='flex items-center h-full mr-2'
>
{(windowSize.width && windowSize.width >= HIDE_LOGO_TEXT_LIMIT && !darkMode) ?
<img alt='' <img alt=''
src='/logo_full.svg' src='/logo_full.svg'
className='max-h-[1.6rem] min-w-[1.6rem]' className='max-h-[1.6rem] min-w-[1.6rem]'
/>} /> : null}
{ (windowSize.width && windowSize.width >= HIDE_LOGO_TEXT_LIMIT) && darkMode && {(windowSize.width && windowSize.width >= HIDE_LOGO_TEXT_LIMIT && darkMode) ?
<img alt='' <img alt=''
src='/logo_full_dark.svg' src='/logo_full_dark.svg'
className='max-h-[1.6rem] min-w-[1.6rem]' className='max-h-[1.6rem] min-w-[1.6rem]'
/>} /> : null}
{ (!windowSize.width || windowSize.width < HIDE_LOGO_TEXT_LIMIT) && {(!windowSize.width || windowSize.width < HIDE_LOGO_TEXT_LIMIT) ?
<img alt='' <img alt=''
src='/logo_sign.svg' src='/logo_sign.svg'
className='max-h-[1.6rem] min-w-[2.2rem]' className='max-h-[1.6rem] min-w-[2.2rem]'
/>} /> : null}
</Link> </Link>
); );
} }

View File

@ -15,25 +15,23 @@ function Navigation () {
return ( return (
<nav className='sticky top-0 left-0 right-0 select-none clr-app z-navigation h-fit'> <nav className='sticky top-0 left-0 right-0 select-none clr-app z-navigation h-fit'>
{!noNavigation && {noNavigation ?
<button <button type='button' tabIndex={-1}
title='Скрыть навигацию'
className='absolute top-0 right-0 z-navigation w-[1.2rem] h-[3rem] border-b-2 border-l-2 clr-btn-nav rounded-none'
onClick={toggleNoNavigation}
tabIndex={-1}
>
<p>{'>'}</p><p>{'>'}</p>
</button>}
{noNavigation &&
<button
title='Показать навигацию' title='Показать навигацию'
className='absolute top-0 right-0 z-navigation px-1 h-[1.6rem] border-b-2 border-l-2 clr-btn-nav rounded-none' className='absolute top-0 right-0 z-navigation px-1 h-[1.6rem] border-b-2 border-l-2 clr-btn-nav rounded-none'
onClick={toggleNoNavigation} onClick={toggleNoNavigation}
tabIndex={-1}
> >
{''} {''}
</button>} </button> : null}
{!noNavigation && {!noNavigation ?
<button type='button' tabIndex={-1}
title='Скрыть навигацию'
className='absolute top-0 right-0 z-navigation w-[1.2rem] h-[3rem] border-b-2 border-l-2 clr-btn-nav rounded-none'
onClick={toggleNoNavigation}
>
<p>{'>'}</p><p>{'>'}</p>
</button> : null}
{!noNavigation ?
<div className='flex items-stretch justify-between pl-2 pr-[0.8rem] border-b-2 rounded-none h-[3rem]'> <div className='flex items-stretch justify-between pl-2 pr-[0.8rem] border-b-2 rounded-none h-[3rem]'>
<div className='flex items-center justify-start'> <div className='flex items-center justify-start'>
<Logo /> <Logo />
@ -59,7 +57,7 @@ function Navigation () {
/> />
<UserMenu /> <UserMenu />
</div> </div>
</div>} </div> : null}
</nav> </nav>
); );
} }

View File

@ -8,15 +8,13 @@ interface NavigationButtonProps {
function NavigationButton({ id, icon, description, onClick, text }: NavigationButtonProps) { function NavigationButton({ id, icon, description, onClick, text }: NavigationButtonProps) {
return ( return (
<button id={id} <button id={id} type='button' tabIndex={-1}
title={description} title={description}
type='button'
onClick={onClick} onClick={onClick}
tabIndex={-1}
className={`flex items-center h-full gap-1 ${text ? 'px-2' : 'px-4'} mr-1 small-caps whitespace-nowrap clr-btn-nav`} className={`flex items-center h-full gap-1 ${text ? 'px-2' : 'px-4'} mr-1 small-caps whitespace-nowrap clr-btn-nav`}
> >
{icon && <span>{icon}</span>} {icon ? <span>{icon}</span> : null}
{text && <span className='font-semibold'>{text}</span>} {text ? <span className='font-semibold'>{text}</span> : null}
</button> </button>
); );
} }

View File

@ -4,12 +4,21 @@ import NavigationButton from './NavigationButton';
function ThemeSwitcher() { function ThemeSwitcher() {
const { darkMode, toggleDarkMode } = useConceptTheme(); const { darkMode, toggleDarkMode } = useConceptTheme();
return ( if (darkMode) {
<> return (
{darkMode && <NavigationButton icon={<LightThemeIcon />} description='Светлая тема' onClick={toggleDarkMode} />} <NavigationButton
{!darkMode && <NavigationButton icon={<DarkThemeIcon />} description='Темная тема' onClick={toggleDarkMode} />} description='Светлая тема'
</> icon={<LightThemeIcon />}
); onClick={toggleDarkMode}
/>);
} else {
return (
<NavigationButton
description='Темная тема'
icon={<DarkThemeIcon />}
onClick={toggleDarkMode}
/>);
}
} }
export default ThemeSwitcher; export default ThemeSwitcher;

View File

@ -14,24 +14,24 @@ function UserMenu() {
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
<div className='flex items-center justify-end h-full w-fit'> <div className='flex items-center justify-end h-full w-fit'>
{ !user && {!user ?
<NavigationButton <NavigationButton
text='Войти...' text='Войти...'
description='Перейти на страницу логина' description='Перейти на страницу логина'
icon={<InDoorIcon />} icon={<InDoorIcon />}
onClick={navigateLogin} onClick={navigateLogin}
/>} /> : null}
{ user && {user ?
<NavigationButton <NavigationButton
icon={<UserIcon />}
description={`Пользователь ${user?.username}`} description={`Пользователь ${user?.username}`}
icon={<UserIcon />}
onClick={menu.toggle} onClick={menu.toggle}
/>} /> : null}
</div> </div>
{ user && menu.isActive && {(user && menu.isActive) ?
<UserDropdown <UserDropdown
hideDropdown={() => menu.hide()} hideDropdown={() => menu.hide()}
/>} /> : null}
</div> </div>
); );
} }

View File

@ -152,12 +152,12 @@ function RSInput({
return ( return (
<div className={`flex flex-col ${dimensions} ${cursor}`}> <div className={`flex flex-col ${dimensions} ${cursor}`}>
{label && {label ?
<Label <Label
text={label} text={label}
htmlFor={id} htmlFor={id}
className='mb-2' className='mb-2'
/>} /> : null}
<CodeMirror id={id} <CodeMirror id={id}
ref={thisRef} ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}

View File

@ -177,7 +177,7 @@ function RefsInput({
return ( return (
<> <>
{ showEditor && {showEditor ?
<DlgEditReference <DlgEditReference
hideWindow={() => setShowEditor(false)} hideWindow={() => setShowEditor(false)}
items={items ?? []} items={items ?? []}
@ -189,9 +189,8 @@ function RefsInput({
mainRefs: mainRefs mainRefs: mainRefs
}} }}
onSave={handleInputReference} onSave={handleInputReference}
/> /> : null}
} {showResolve ?
{ showResolve &&
<Modal <Modal
readonly readonly
hideWindow={() => setShowResolve(false)} hideWindow={() => setShowResolve(false)}
@ -199,16 +198,15 @@ function RefsInput({
<div className='max-h-[60vh] max-w-[80vw] overflow-auto'> <div className='max-h-[60vh] max-w-[80vw] overflow-auto'>
<PrettyJson data={refsData} /> <PrettyJson data={refsData} />
</div> </div>
</Modal>} </Modal> : null}
<div className={`flex flex-col w-full ${cursor}`}> <div className={`flex flex-col w-full ${cursor}`}>
{label && {label ?
<Label <Label
text={label} text={label}
htmlFor={id} htmlFor={id}
className='mb-2' className='mb-2'
/>} /> : null}
<CodeMirror id={id} <CodeMirror id={id} ref={thisRef}
ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}
theme={customTheme} theme={customTheme}
extensions={editorExtensions} extensions={editorExtensions}

View File

@ -8,19 +8,17 @@ interface RequireAuthProps {
function RequireAuth({ children }: RequireAuthProps) { function RequireAuth({ children }: RequireAuthProps) {
const { user } = useAuth(); const { user } = useAuth();
return ( if (user) {
<> return children;
{user && children} } else {
{!user && return (
<div className='flex flex-col items-center gap-1 mt-2'> <div className='flex flex-col items-center gap-1 mt-2'>
<p className='mb-2'>Пожалуйста войдите в систему</p> <p className='mb-2'>Пожалуйста войдите в систему</p>
<TextURL text='Войти в Портал' href='/login'/> <TextURL text='Войти в Портал' href='/login'/>
<TextURL text='Зарегистрироваться' href='/signup'/> <TextURL text='Зарегистрироваться' href='/signup'/>
<TextURL text='Начальная страница' href='/'/> <TextURL text='Начальная страница' href='/'/>
</div> </div>);
} }
</>
);
} }
export default RequireAuth; export default RequireAuth;

View File

@ -27,14 +27,18 @@ function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: Constituent
> >
{value.alias} {value.alias}
</div> </div>
{ !shortTooltip && <ConstituentaTooltip data={value} anchor={`#${prefixID}${value.alias}`} />} {!shortTooltip ?
{ shortTooltip && <ConstituentaTooltip
anchor={`#${prefixID}${value.alias}`}
data={value}
/> : null}
{shortTooltip ?
<ConceptTooltip <ConceptTooltip
anchorSelect={`#${prefixID}${value.alias}`} anchorSelect={`#${prefixID}${value.alias}`}
place='right' place='right'
> >
<p><span className='font-semibold'>Статус</span>: {describeExpressionStatus(value.status)}</p> <p><span className='font-semibold'>Статус</span>: {describeExpressionStatus(value.status)}</p>
</ConceptTooltip>} </ConceptTooltip> : null}
</div>); </div>);
} }

View File

@ -127,7 +127,6 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
return ( return (
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<div className='overflow-y-auto text-sm border select-none max-h-[5.8rem] min-h-[5.8rem]'> <div className='overflow-y-auto text-sm border select-none max-h-[5.8rem] min-h-[5.8rem]'>
<DataTable dense noFooter <DataTable dense noFooter
data={state.arguments} data={state.arguments}

View File

@ -58,8 +58,8 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
<div className='flex justify-center w-full gap-6'> <div className='flex justify-center w-full gap-6'>
<SelectSingle <SelectSingle
className='my-2 min-w-[15rem] self-center' className='my-2 min-w-[15rem] self-center'
options={SelectorCstType}
placeholder='Выберите тип' placeholder='Выберите тип'
options={SelectorCstType}
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }} value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }}
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})} onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})}
/> />

View File

@ -30,9 +30,9 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
return ( return (
<Modal <Modal
title='Удаление конституент' title='Удаление конституент'
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={true} canSubmit={true}
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='max-w-[60vw] min-w-[20rem]'> <div className='max-w-[60vw] min-w-[20rem]'>
@ -41,10 +41,10 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
{selected.map( {selected.map(
(id) => { (id) => {
const cst = schema!.items.find(cst => cst.id === id); const cst = schema!.items.find(cst => cst.id === id);
return (cst && return (cst ?
<p key={`${prefixes.cst_delete_list}${cst.id}`}> <p key={`${prefixes.cst_delete_list}${cst.id}`}>
{labelConstituenta(cst)} {labelConstituenta(cst)}
</p>); </p> : null);
})} })}
</div> </div>
<p className='mt-4'>Зависимые конституенты: <b>{expansion.length}</b></p> <p className='mt-4'>Зависимые конституенты: <b>{expansion.length}</b></p>
@ -52,10 +52,10 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
{expansion.map( {expansion.map(
(id) => { (id) => {
const cst = schema!.items.find(cst => cst.id === id); const cst = schema!.items.find(cst => cst.id === id);
return (cst && return (cst ?
<p key={`${prefixes.cst_dependant_list}${cst.id}`}> <p key={`${prefixes.cst_dependant_list}${cst.id}`}>
{labelConstituenta(cst)} {labelConstituenta(cst)}
</p>); </p> : null);
})} })}
</div> </div>
<Checkbox <Checkbox

View File

@ -186,37 +186,31 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
<HelpTerminologyControl /> <HelpTerminologyControl />
</ConceptTooltip> </ConceptTooltip>
</div> </div>
{type !== ReferenceType.SYNTACTIC ? null : {type === ReferenceType.SYNTACTIC ?
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<div className='flex flex-start'> <div className='flex flex-start'>
<TextInput id='offset' type='number' <TextInput type='number' dense
label='Смещение' label='Смещение'
dimensions='max-w-[10rem]' dimensions='max-w-[10rem]'
dense
value={offset} value={offset}
onChange={event => setOffset(event.target.valueAsNumber)} onChange={event => setOffset(event.target.valueAsNumber)}
/> />
<div className='self-center ml-2 text-sm font-semibold whitespace-nowrap'> <div className='self-center ml-2 text-sm font-semibold whitespace-nowrap'>
Основная ссылка: Основная ссылка:
</div> </div>
<TextInput <TextInput disabled dense noBorder
dense
disabled
noBorder
value={mainLink} value={mainLink}
dimensions='w-full text-sm' dimensions='w-full text-sm'
/> />
</div> </div>
<TextInput id='nominal' type='text' <TextInput spellCheck
label='Начальная форма' label='Начальная форма'
placeholder='зависимое слово в начальной форме' placeholder='зависимое слово в начальной форме'
dimensions='w-full'
spellCheck
value={nominal} value={nominal}
onChange={event => setNominal(event.target.value)} onChange={event => setNominal(event.target.value)}
/> />
</div>} </div> : null}
{type !== ReferenceType.ENTITY ? null : {type === ReferenceType.ENTITY ?
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<ConstituentaPicker <ConstituentaPicker
value={selectedCst} value={selectedCst}
@ -230,11 +224,10 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
/> />
<div className='flex gap-4 flex-start'> <div className='flex gap-4 flex-start'>
<TextInput <TextInput dense
label='Отсылаемая конституента' label='Отсылаемая конституента'
placeholder='Имя' placeholder='Имя'
dimensions='max-w-[16rem] min-w-[16rem] whitespace-nowrap' dimensions='max-w-[16rem] min-w-[16rem] whitespace-nowrap'
dense
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
@ -242,10 +235,7 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
<div className='self-center text-sm font-semibold'> <div className='self-center text-sm font-semibold'>
Термин: Термин:
</div> </div>
<TextInput <TextInput disabled dense noBorder
dense
disabled
noBorder
value={term} value={term}
tooltip={term} tooltip={term}
dimensions='w-full text-sm' dimensions='w-full text-sm'
@ -265,7 +255,7 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))} onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))}
/> />
</div> </div>
</div>} </div> : null}
</div> </div>
</Modal>); </Modal>);
} }

View File

@ -10,10 +10,8 @@ interface WordformButtonProps {
function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...props }: WordformButtonProps) { function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...props }: WordformButtonProps) {
return ( return (
<button <button type='button' tabIndex={-1}
type='button'
onClick={() => onSelectGrams(grams)} onClick={() => onSelectGrams(grams)}
tabIndex={-1}
className={`min-w-[6rem] p-1 border rounded-none cursor-pointer clr-btn-clear clr-hover ${isSelected ? 'clr-selected': ''}`} className={`min-w-[6rem] p-1 border rounded-none cursor-pointer clr-btn-clear clr-hover ${isSelected ? 'clr-selected': ''}`}
{...props} {...props}
> >

View File

@ -11,7 +11,7 @@ import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossI
import { useConceptTheme } from '../context/ThemeContext'; import { useConceptTheme } from '../context/ThemeContext';
import useConceptText from '../hooks/useConceptText'; import useConceptText from '../hooks/useConceptText';
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '../models/language'; import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '../models/language';
import { getCompatibleGrams, wordFormEquals, parseGrammemes } from '../models/languageAPI'; import { getCompatibleGrams, parseGrammemes,wordFormEquals } from '../models/languageAPI';
import { IConstituenta, TermForm } from '../models/rsform'; import { IConstituenta, TermForm } from '../models/rsform';
import { colorfgGrammeme } from '../utils/color'; import { colorfgGrammeme } from '../utils/color';
import { labelGrammeme } from '../utils/labels'; import { labelGrammeme } from '../utils/labels';
@ -169,12 +169,11 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
maxSize: 250, maxSize: 250,
cell: props => cell: props =>
<div className='flex flex-wrap justify-start gap-1 select-none'> <div className='flex flex-wrap justify-start gap-1 select-none'>
{ props.getValue().map( {props.getValue().map(
gram => (gram) =>
<div <div
key={`${props.cell.id}-${gram}`} key={`${props.cell.id}-${gram}`}
className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap' className='min-w-[3rem] px-1 text-sm text-center rounded-md whitespace-nowrap'
title=''
style={{ style={{
borderWidth: '1px', borderWidth: '1px',
borderColor: colorfgGrammeme(gram, colors), borderColor: colorfgGrammeme(gram, colors),
@ -195,10 +194,9 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
maxSize: 50, maxSize: 50,
cell: props => cell: props =>
<div> <div>
<MiniButton <MiniButton noHover
tooltip='Удалить словоформу' tooltip='Удалить словоформу'
icon={<CrossIcon size={4} color='text-warning'/>} icon={<CrossIcon size={4} color='text-warning'/>}
noHover
onClick={() => handleDeleteRow(props.row.index)} onClick={() => handleDeleteRow(props.row.index)}
/> />
</div> </div>
@ -247,7 +245,6 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
placeholder='Введите текст' placeholder='Введите текст'
rows={2} rows={2}
dimensions='min-w-[18rem] w-full min-h-[4.2rem]' dimensions='min-w-[18rem] w-full min-h-[4.2rem]'
value={inputText} value={inputText}
onChange={event => setInputText(event.target.value)} onChange={event => setInputText(event.target.value)}
/> />
@ -317,11 +314,10 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
</div> </div>
<div className='border overflow-y-auto max-h-[17.4rem] min-h-[17.4rem] mb-2'> <div className='border overflow-y-auto max-h-[17.4rem] min-h-[17.4rem] mb-2'>
<DataTable <DataTable dense noFooter
data={forms} data={forms}
columns={columns} columns={columns}
headPosition='0' headPosition='0'
dense
noDataComponent={ noDataComponent={
<span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'> <span className='flex flex-col justify-center p-2 text-center min-h-[2rem]'>
<p>Список пуст</p> <p>Список пуст</p>

View File

@ -20,11 +20,10 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm } : DlgGraphOptionsPro
}; };
return ( return (
<Modal <Modal canSubmit
hideWindow={hideWindow} hideWindow={hideWindow}
title='Настройки графа термов' title='Настройки графа термов'
onSubmit={handleSubmit} onSubmit={handleSubmit}
canSubmit
submitText='Применить' submitText='Применить'
> >
<div className='flex gap-2'> <div className='flex gap-2'>
@ -34,25 +33,25 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm } : DlgGraphOptionsPro
label='Скрыть текст' label='Скрыть текст'
tooltip='Не отображать термины' tooltip='Не отображать термины'
value={params.noTerms} value={params.noTerms}
setValue={ value => updateParams({noTerms: value}) } setValue={value => updateParams({noTerms: value})}
/> />
<Checkbox <Checkbox
label='Скрыть несвязанные' label='Скрыть несвязанные'
tooltip='Неиспользуемые конституенты' tooltip='Неиспользуемые конституенты'
value={params.noHermits} value={params.noHermits}
setValue={ value => updateParams({ noHermits: value}) } setValue={value => updateParams({ noHermits: value})}
/> />
<Checkbox <Checkbox
label='Скрыть шаблоны' label='Скрыть шаблоны'
tooltip='Терм-функции и предикат-функции с параметризованными аргументами' tooltip='Терм-функции и предикат-функции с параметризованными аргументами'
value={params.noTemplates} value={params.noTemplates}
setValue={ value => updateParams({ noTemplates: value}) } setValue={value => updateParams({ noTemplates: value})}
/> />
<Checkbox <Checkbox
label='Транзитивная редукция' label='Транзитивная редукция'
tooltip='Удалить связи, образующие транзитивные пути в графе' tooltip='Удалить связи, образующие транзитивные пути в графе'
value={params.noTransitive} value={params.noTransitive}
setValue={ value => updateParams({ noTransitive: value}) } setValue={value => updateParams({ noTransitive: value})}
/> />
</div> </div>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
@ -60,42 +59,42 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm } : DlgGraphOptionsPro
<Checkbox <Checkbox
label={labelCstType(CstType.BASE)} label={labelCstType(CstType.BASE)}
value={params.allowBase} value={params.allowBase}
setValue={ value => updateParams({ allowBase: value}) } setValue={value => updateParams({ allowBase: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.STRUCTURED)} label={labelCstType(CstType.STRUCTURED)}
value={params.allowStruct} value={params.allowStruct}
setValue={ value => updateParams({ allowStruct: value}) } setValue={value => updateParams({ allowStruct: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.TERM)} label={labelCstType(CstType.TERM)}
value={params.allowTerm} value={params.allowTerm}
setValue={ value => updateParams({ allowTerm: value}) } setValue={value => updateParams({ allowTerm: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.AXIOM)} label={labelCstType(CstType.AXIOM)}
value={params.allowAxiom} value={params.allowAxiom}
setValue={ value => updateParams({ allowAxiom: value}) } setValue={value => updateParams({ allowAxiom: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.FUNCTION)} label={labelCstType(CstType.FUNCTION)}
value={params.allowFunction} value={params.allowFunction}
setValue={ value => updateParams({ allowFunction: value}) } setValue={value => updateParams({ allowFunction: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.PREDICATE)} label={labelCstType(CstType.PREDICATE)}
value={params.allowPredicate} value={params.allowPredicate}
setValue={ value => updateParams({ allowPredicate: value}) } setValue={value => updateParams({ allowPredicate: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.CONSTANT)} label={labelCstType(CstType.CONSTANT)}
value={params.allowConstant} value={params.allowConstant}
setValue={ value => updateParams({ allowConstant: value}) } setValue={value => updateParams({ allowConstant: value})}
/> />
<Checkbox <Checkbox
label={labelCstType(CstType.THEOREM)} label={labelCstType(CstType.THEOREM)}
value={params.allowTheorem} value={params.allowTheorem}
setValue ={ value => updateParams({ allowTheorem: value}) } setValue={value => updateParams({ allowTheorem: value})}
/> />
</div> </div>
</div> </div>

View File

@ -42,23 +42,25 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
return ( return (
<Modal <Modal
title='Переименование конституенты' title='Переименование конституенты'
submitText='Переименовать'
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'}
submitText='Переименовать'
> >
<div className='flex items-center gap-4 px-2 my-2 h-fit min-w-[25rem]'> <div className='flex items-center gap-4 px-2 my-2 h-fit min-w-[25rem]'>
<SelectSingle <SelectSingle
placeholder='Выберите тип'
className='min-w-[14rem] self-center z-modal-top' className='min-w-[14rem] self-center z-modal-top'
options={SelectorCstType} options={SelectorCstType}
placeholder='Выберите тип' value={{
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }} value: cstData.cst_type,
label: labelCstType(cstData.cst_type)
}}
onChange={data => updateData({cst_type: data?.value ?? CstType.BASE})} onChange={data => updateData({cst_type: data?.value ?? CstType.BASE})}
/> />
<div> <div>
<TextInput id='alias' label='Имя' <TextInput id='alias' label='Имя' dense
dense
dimensions='w-[7rem]' dimensions='w-[7rem]'
value={cstData.alias} value={cstData.alias}
onChange={event => updateData({alias: event.target.value})} onChange={event => updateData({alias: event.target.value})}

View File

@ -59,16 +59,22 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
> >
<div className='flex flex-col items-start gap-2'> <div className='flex flex-col items-start gap-2'>
<div className='w-full text-lg text-center'> <div className='w-full text-lg text-center'>
{!hoverNode && expression} {!hoverNode ? expression : null}
{hoverNode && {hoverNode ?
<div> <div>
<span>{expression.slice(0, hoverNode.start)}</span> <span>{expression.slice(0, hoverNode.start)}</span>
<span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span> <span className='clr-selected'>{expression.slice(hoverNode.start, hoverNode.finish)}</span>
<span>{expression.slice(hoverNode.finish)}</span> <span>{expression.slice(hoverNode.finish)}</span>
</div>} </div> : null}
</div> </div>
<div className='flex-wrap w-full h-full overflow-auto'> <div className='flex-wrap w-full h-full overflow-auto'>
<div className='relative' style={{width: 'calc(100vw - 6rem - 2px)', height: 'calc(100vh - 14rem - 2px)'}}> <div
className='relative'
style={{
width: 'calc(100vw - 6rem - 2px)',
height: 'calc(100vh - 14rem - 2px)'
}}
>
<GraphCanvas <GraphCanvas
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}

View File

@ -38,28 +38,27 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
} }
return ( return (
<Modal <Modal
title='Импорт схемы из Экстеора' title='Импорт схемы из Экстеора'
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={!!file} canSubmit={!!file}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Загрузить' submitText='Загрузить'
> >
<div className='flex flex-col items-start min-w-[20rem] max-w-[20rem]'> <div className='flex flex-col items-start min-w-[20rem] max-w-[20rem]'>
<FileInput <FileInput
label='Выбрать файл' label='Выбрать файл'
acceptType={EXTEOR_TRS_FILE} acceptType={EXTEOR_TRS_FILE}
onChange={handleFile} onChange={handleFile}
/> />
<Checkbox <Checkbox
label='Загружать название и комментарий' label='Загружать название и комментарий'
value={loadMetadata} dimensions='w-fit pb-2'
setValue={value => setLoadMetadata(value)} value={loadMetadata}
dimensions='w-fit pb-2' setValue={value => setLoadMetadata(value)}
/> />
</div> </div>
</Modal> </Modal>);
);
} }
export default DlgUploadRSForm; export default DlgUploadRSForm;

View File

@ -85,9 +85,7 @@ function CreateRSFormPage() {
> >
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-[-2.4rem] right-[-1rem] flex'> <div className='absolute top-[-2.4rem] right-[-1rem] flex'>
<input <input ref={inputRef} type='file'
type='file'
ref={inputRef}
style={{ display: 'none' }} style={{ display: 'none' }}
accept={EXTEOR_TRS_FILE} accept={EXTEOR_TRS_FILE}
onChange={handleFileChange} onChange={handleFileChange}
@ -100,26 +98,29 @@ function CreateRSFormPage() {
</div> </div>
</div> </div>
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
{ fileName && <Label text={`Загружен файл: ${fileName}`} />} {fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
<TextInput id='title' label='Полное название' type='text' <TextInput
required={!file} label='Полное название'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
required={!file}
value={title} value={title}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput dense
dense label='Сокращение'
placeholder={file && 'Загрузить из файла'}
required={!file} required={!file}
value={alias} value={alias}
placeholder={file && 'Загрузить из файла'}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextArea id='comment' label='Комментарий' <TextArea
value={comment} label='Комментарий'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
value={comment}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox
label='Общедоступная схема'
value={common} value={common}
setValue={value => setCommon(value ?? false)} setValue={value => setCommon(value ?? false)}
/> />
@ -129,13 +130,13 @@ function CreateRSFormPage() {
loading={processing} loading={processing}
dimensions='min-w-[10rem]' dimensions='min-w-[10rem]'
/> />
<Button <Button
text='Отмена' text='Отмена'
onClick={() => handleCancel()}
dimensions='min-w-[10rem]' dimensions='min-w-[10rem]'
onClick={() => handleCancel()}
/> />
</div> </div>
{ error && <BackendError error={error} />} {error ? <BackendError error={error} /> : null}
</div> </div>
</Form> </Form>
</div> </div>

View File

@ -21,10 +21,12 @@ function HomePage() {
}, [navigateTo, user]) }, [navigateTo, user])
return ( return (
<div className='flex flex-col items-center justify-center w-full px-4 py-2'> <div className='flex flex-col items-center justify-center w-full px-4 py-2'>
{ user?.is_staff && <p>Лендинг находится в разработке. Данная страница видна только пользователям с правами администратора.</p> } {user?.is_staff ?
</div> <p>
); Лендинг находится в разработке. Данная страница видна только пользователям с правами администратора.
</p>: null}
</div>);
} }
export default HomePage; export default HomePage;

View File

@ -38,28 +38,26 @@ function LibraryPage() {
}, []) }, [])
return ( return (
<div className='w-full'> <>
{ library.loading && <ConceptLoader /> } {library.loading ? <ConceptLoader/> : null}
{ library.error && <BackendError error={library.error} />} {library.error ? <BackendError error={library.error}/> : null}
{ !library.loading && library.items && {(!library.loading && library.items) ?
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<SearchPanel <SearchPanel
query={query} query={query}
setQuery={setQuery} setQuery={setQuery}
strategy={strategy} strategy={strategy}
setStrategy={setStrategy} setStrategy={setStrategy}
total={library.items.length ?? 0} total={library.items.length ?? 0}
filtered={items.length} filtered={items.length}
setFilter={setFilter} setFilter={setFilter}
/> />
<ViewLibrary <ViewLibrary
resetQuery={resetQuery} resetQuery={resetQuery}
items={items} items={items}
/> />
</div> </div> : null}
} </>);
</div>
);
} }
export default LibraryPage; export default LibraryPage;

View File

@ -39,18 +39,16 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
return ( return (
<div ref={strategyMenu.ref} className='h-full text-right'> <div ref={strategyMenu.ref} className='h-full text-right'>
<SelectorButton <SelectorButton transparent tabIndex={-1}
tooltip='Список фильтров' tooltip='Список фильтров'
dimensions='w-fit h-full' dimensions='w-fit h-full'
transparent
icon={<FilterIcon size={5} />} icon={<FilterIcon size={5} />}
text={labelLibraryFilter(value)} text={labelLibraryFilter(value)}
tabIndex={-1}
onClick={strategyMenu.toggle} onClick={strategyMenu.toggle}
/> />
{ strategyMenu.isActive && {strategyMenu.isActive ?
<Dropdown> <Dropdown>
{ Object.values(LibraryFilterStrategy).map( {Object.values(LibraryFilterStrategy).map(
(enumValue, index) => { (enumValue, index) => {
const strategy = enumValue as LibraryFilterStrategy; const strategy = enumValue as LibraryFilterStrategy;
return ( return (
@ -63,7 +61,7 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
disabled={isStrategyDisabled(strategy)} disabled={isStrategyDisabled(strategy)}
/>); />);
})} })}
</Dropdown>} </Dropdown> : null}
</div> </div>
); );
} }

View File

@ -69,33 +69,31 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setStrategy,
}, [strategy, navigateTo]); }, [strategy, navigateTo]);
return ( return (
<div className='sticky top-0 left-0 right-0 flex items-stretch justify-start w-full border-b clr-input max-h-[2.3rem] pr-40'> <div className='sticky top-0 left-0 right-0 flex items-stretch justify-start w-full border-b clr-input max-h-[2.3rem] pr-40'>
<div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'> <div className='px-2 py-1 select-none whitespace-nowrap min-w-[10rem]'>
Фильтр Фильтр
<span className='ml-2'> <span className='ml-2'>
{filtered} из {total} {filtered} из {total}
</span> </span>
</div> </div>
<div className='flex items-center justify-center w-full gap-1'> <div className='flex items-center justify-center w-full gap-1'>
<div className='relative min-w-[10rem] select-none'> <div className='relative 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>
<input
type='text'
value={query}
className='w-full p-2 pl-10 text-sm outline-none clr-input'
placeholder='Поиск'
onChange={handleChangeQuery}
/>
</div> </div>
<PickerStrategy <input
value={strategy} placeholder='Поиск'
onChange={handleChangeStrategy} value={query}
className='w-full p-2 pl-10 text-sm outline-none clr-input'
onChange={handleChangeQuery}
/> />
</div> </div>
<PickerStrategy
value={strategy}
onChange={handleChangeStrategy}
/>
</div> </div>
); </div>);
} }
export default SearchPanel; export default SearchPanel;

View File

@ -45,9 +45,18 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
className='flex items-center justify-start gap-1 min-w-[2.75rem]' className='flex items-center justify-start gap-1 min-w-[2.75rem]'
id={`${prefixes.library_list}${item.id}`} id={`${prefixes.library_list}${item.id}`}
> >
{user && user.subscriptions.includes(item.id) && <p title='Отслеживаемая'><SubscribedIcon size={3}/></p>} {(user && user.subscriptions.includes(item.id)) ?
{item.is_common && <p title='Общедоступная'><GroupIcon size={3}/></p>} <p title='Отслеживаемая'>
{item.is_canonical && <p title='Неизменная'><EducationIcon size={3}/></p>} <SubscribedIcon size={3}/>
</p> : null}
{item.is_common ?
<p title='Общедоступная'>
<GroupIcon size={3}/>
</p> : null}
{item.is_canonical ?
<p title='Неизменная'>
<EducationIcon size={3}/>
</p> : null}
</div> </div>
</>); </>);
}, },
@ -94,51 +103,50 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
], [intl, getUserLabel, user]); ], [intl, getUserLabel, user]);
return ( return (
<div> <>
{items.length > 0 && {items.length !== 0 ?
<div className='sticky top-[2.3rem] w-full'> <div className='sticky top-[2.3rem] w-full'>
<div className='absolute top-[-0.125rem] left-0 flex gap-1 ml-3 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={5} /> <HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#library-help'>
<div className='max-w-[35rem]'>
<HelpLibrary />
</div>
</ConceptTooltip>
</div> </div>
</div>} <ConceptTooltip anchorSelect='#library-help'>
<DataTable <div className='max-w-[35rem]'>
columns={columns} <HelpLibrary />
data={items} </div>
</ConceptTooltip>
headPosition='2.3rem'
noDataComponent={
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[6rem]'>
<p>Список схем пуст</p>
<p className='flex justify-center gap-4'>
<TextURL text='Создать схему' href='/rsform-create'/>
<span className='cursor-pointer hover:underline text-url' onClick={cleanQuery}>
Очистить фильтр
</span>
</p>
</div>}
onRowClicked={openRSForm}
enableSorting
initialSorting={{
id: 'time_update',
desc: true
}}
enablePagination
paginationPerPage={itemsPerPage}
onChangePaginationOption={setItemsPerPage}
paginationOptions={[10, 20, 30, 50, 100]}
/>
</div> </div>
); </div> : null}
<DataTable
columns={columns}
data={items}
headPosition='2.3rem'
noDataComponent={
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[6rem]'>
<p>Список схем пуст</p>
<p className='flex justify-center gap-4'>
<TextURL text='Создать схему' href='/rsform-create'/>
<span className='cursor-pointer hover:underline text-url' onClick={cleanQuery}>
Очистить фильтр
</span>
</p>
</div>}
onRowClicked={openRSForm}
enableSorting
initialSorting={{
id: 'time_update',
desc: true
}}
enablePagination
paginationPerPage={itemsPerPage}
onChangePaginationOption={setItemsPerPage}
paginationOptions={[10, 20, 30, 50, 100]}
/>
</>);
} }
export default ViewLibrary; export default ViewLibrary;

View File

@ -66,66 +66,62 @@ function LoginPage() {
} }
return ( return (
<div className='flex items-start justify-center w-full pt-8 select-none' style={{minHeight: mainHeight}}> <div
{ user && className='flex items-start justify-center w-full pt-8 select-none'
<div className='flex flex-col items-center gap-2'> style={{minHeight: mainHeight}}
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p> >
<p> {user ?
<TextURL text='Создать схему' href='/rsform-create'/> <div className='flex flex-col items-center gap-2'>
<span> | </span> <p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
<TextURL text='Библиотека' href='/library'/> <p>
<span> | </span> <TextURL text='Создать схему' href='/rsform-create'/>
<TextURL text='Справка' href='/manuals'/> <span> | </span>
<span> | </span> <TextURL text='Библиотека' href='/library'/>
<span <span> | </span>
className='cursor-pointer hover:underline text-url' <TextURL text='Справка' href='/manuals'/>
onClick={logoutAndRedirect} <span> | </span>
> <span
Выйти className='cursor-pointer hover:underline text-url'
</span> onClick={logoutAndRedirect}
</p> >
</div>} Выйти
{ !user && </span>
<Form </p>
onSubmit={handleSubmit} </div> : null}
dimensions='w-[24rem]' {!user ?
> <Form
<img alt='Концепт Портал' onSubmit={handleSubmit}
src='/logo_full.svg' dimensions='w-[24rem]'
className='max-h-[2.5rem] min-w-[2.5rem] mt-2 mb-4' >
/> <img alt='Концепт Портал'
<TextInput id='username' type='text' src='/logo_full.svg'
label='Имя пользователя' className='max-h-[2.5rem] min-w-[2.5rem] mt-2 mb-4'
required />
allowEnter <TextInput id='username' autoFocus required allowEnter
value={username} label='Имя пользователя'
autoFocus value={username}
onChange={event => setUsername(event.target.value)} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' type='password' <TextInput id='password' type='password' required allowEnter
label='Пароль' label='Пароль'
required value={password}
allowEnter onChange={event => setPassword(event.target.value)}
value={password} />
onChange={event => setPassword(event.target.value)}
/>
<div className='flex justify-center w-full py-2'> <div className='flex justify-center w-full py-2'>
<SubmitButton <SubmitButton
text='Войти' text='Войти'
dimensions='w-[12rem]' dimensions='w-[12rem]'
loading={loading} loading={loading}
/> />
</div> </div>
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
<TextURL text='Восстановить пароль...' href='/restore-password' /> <TextURL text='Восстановить пароль...' href='/restore-password' />
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' /> <TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
</div> </div>
{ error && <ProcessError error={error} />} {error ? <ProcessError error={error} /> : null}
</Form> </Form> : null}
} </div>);
</div>
);
} }
export default LoginPage; export default LoginPage;

View File

@ -34,16 +34,16 @@ function ManualsPage() {
}, [search, setActiveTopic, navigateTopic]); }, [search, setActiveTopic, navigateTopic]);
return ( return (
<div className='flex w-full gap-2 justify-start items-start' style={{minHeight: mainHeight}}> <div
<TopicsList className='flex items-start justify-start w-full gap-2'
activeTopic={activeTopic} style={{minHeight: mainHeight}}
onChangeTopic={topic => onSelectTopic(topic)} >
/> <TopicsList
<ViewTopic activeTopic={activeTopic}
topic={activeTopic} onChangeTopic={topic => onSelectTopic(topic)}
/> />
</div> <ViewTopic topic={activeTopic} />
); </div>);
} }
export default ManualsPage; export default ManualsPage;

View File

@ -11,16 +11,17 @@ function TopicsList({ activeTopic, onChangeTopic }: TopicsListProps) {
return ( return (
<div className='sticky top-0 left-0 border-x min-w-[13rem] select-none flex flex-col clr-controls small-caps h-fit'> <div className='sticky top-0 left-0 border-x min-w-[13rem] select-none flex flex-col clr-controls small-caps h-fit'>
<div className='my-2 text-lg text-center'>Справка</div> <div className='my-2 text-lg text-center'>Справка</div>
{ Object.values(HelpTopic).map( {Object.values(HelpTopic).map(
(topic, index) => { (topic, index) => {
const isActive = activeTopic === topic;
return ( return (
<div key={`${prefixes.topic_list}${index}`} <div key={`${prefixes.topic_list}${index}`}
className={`px-3 py-1 border-y cursor-pointer clr-hover ${activeTopic === topic ? 'clr-selected ' : ''}`} className={`px-3 py-1 border-y cursor-pointer clr-hover ${isActive ? 'clr-selected ' : ''}`}
title={describeHelpTopic(topic)} title={describeHelpTopic(topic)}
onClick={() => onChangeTopic(topic)} onClick={() => onChangeTopic(topic)}
> >
{labelHelpTopic(topic)} {labelHelpTopic(topic)}
</div>) </div>);
})} })}
</div> </div>
); );

View File

@ -17,20 +17,19 @@ interface ViewTopicProps {
function ViewTopic({ topic }: ViewTopicProps) { function ViewTopic({ topic }: ViewTopicProps) {
return ( return (
<div className='w-full px-2 py-2'> <div className='w-full px-2 py-2'>
{topic === HelpTopic.MAIN && <HelpMain />} {topic === HelpTopic.MAIN ? <HelpMain /> : null}
{topic === HelpTopic.LIBRARY && <HelpLibrary />} {topic === HelpTopic.LIBRARY ? <HelpLibrary /> : null}
{topic === HelpTopic.RSFORM && <HelpRSFormMeta />} {topic === HelpTopic.RSFORM ? <HelpRSFormMeta /> : null}
{topic === HelpTopic.CSTLIST && <HelpRSFormItems />} {topic === HelpTopic.CSTLIST ? <HelpRSFormItems /> : null}
{topic === HelpTopic.CONSTITUENTA && <HelpConstituenta />} {topic === HelpTopic.CONSTITUENTA ? <HelpConstituenta /> : null}
{topic === HelpTopic.GRAPH_TERM && <HelpTermGraph />} {topic === HelpTopic.GRAPH_TERM ? <HelpTermGraph /> : null}
{topic === HelpTopic.RSTEMPLATES && <HelpRSTemplates />} {topic === HelpTopic.RSTEMPLATES ? <HelpRSTemplates /> : null}
{topic === HelpTopic.RSLANG && <HelpRSLang />} {topic === HelpTopic.RSLANG ? <HelpRSLang /> : null}
{topic === HelpTopic.TERM_CONTROL && <HelpTerminologyControl />} {topic === HelpTopic.TERM_CONTROL ? <HelpTerminologyControl /> : null}
{topic === HelpTopic.EXTEOR && <HelpExteor />} {topic === HelpTopic.EXTEOR ? <HelpExteor /> : null}
{topic === HelpTopic.API && <HelpAPI />} {topic === HelpTopic.API ? <HelpAPI /> : null}
</div> </div>);
);
} }
export default ViewTopic; export default ViewTopic;

View File

@ -2,12 +2,11 @@ import TextURL from '../components/common/TextURL';
export function NotFoundPage() { export function NotFoundPage() {
return ( return (
<div className='flex flex-col px-4 py-2'> <div className='flex flex-col px-4 py-2'>
<h1 className='text-xl font-semibold'>Ошибка 404 - Страница не найдена</h1> <h1 className='text-xl font-semibold'>Ошибка 404 - Страница не найдена</h1>
<p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базе данных</p> <p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базе данных</p>
<TextURL href='/' text='Вернуться на Портал' /> <TextURL href='/' text='Вернуться на Портал' />
</div> </div>);
);
} }
export default NotFoundPage; export default NotFoundPage;

View File

@ -161,8 +161,14 @@ function EditorConstituenta({
} }
return ( return (
<div className='flex max-w-[1500px]' tabIndex={-1} onKeyDown={handleInput}> <div tabIndex={-1}
<form onSubmit={handleSubmit} className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'> className='flex max-w-[1500px]'
onKeyDown={handleInput}
>
<form
onSubmit={handleSubmit}
className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'
>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 right-0 flex items-start justify-between w-full'> <div className='absolute top-0 right-0 flex items-start justify-between w-full'>
{activeCst && {activeCst &&
@ -179,10 +185,9 @@ function EditorConstituenta({
<span className='small-caps'>Конституента </span> <span className='small-caps'>Конституента </span>
<span className='ml-1 small-caps'>{alias}</span> <span className='ml-1 small-caps'>{alias}</span>
</div> </div>
<MiniButton <MiniButton noHover
tooltip='Переименовать конституенту' tooltip='Переименовать конституенту'
disabled={!isEnabled} disabled={!isEnabled}
noHover
onClick={handleRename} onClick={handleRename}
icon={<EditIcon size={4} color={isEnabled ? 'text-primary' : ''} />} icon={<EditIcon size={4} color={isEnabled ? 'text-primary' : ''} />}
/> />
@ -228,7 +233,8 @@ function EditorConstituenta({
</div> </div>
</div> </div>
<div className='flex flex-col gap-3 mt-1'> <div className='flex flex-col gap-3 mt-1'>
<RefsInput id='term' label='Термин' <RefsInput
label='Термин'
placeholder='Обозначение, используемое в текстовых определениях данной схемы' placeholder='Обозначение, используемое в текстовых определениях данной схемы'
items={schema?.items} items={schema?.items}
value={term} value={term}
@ -237,8 +243,8 @@ function EditorConstituenta({
disabled={!isEnabled} disabled={!isEnabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => setTerm(newValue)}
/> />
<TextArea id='typification' label='Типизация' <TextArea dense noBorder
dense noBorder label='Типизация'
rows={typification.length > 70 ? 2 : 1} rows={typification.length > 70 ? 2 : 1}
value={typification} value={typification}
colors='clr-app' colors='clr-app'
@ -248,7 +254,8 @@ function EditorConstituenta({
}} }}
disabled disabled
/> />
<EditorRSExpression id='expression' label='Формальное определение' <EditorRSExpression
label='Формальное определение'
activeCst={activeCst} activeCst={activeCst}
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression} value={expression}
@ -258,7 +265,8 @@ function EditorConstituenta({
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
/> />
<RefsInput id='definition' label='Текстовое определение' <RefsInput
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения' placeholder='Лингвистическая интерпретация формального выражения'
items={schema?.items} items={schema?.items}
value={textDefinition} value={textDefinition}
@ -267,11 +275,11 @@ function EditorConstituenta({
disabled={!isEnabled} disabled={!isEnabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение' placeholder='Договоренность об интерпретации или пояснение'
value={convention} value={convention}
disabled={!isEnabled} disabled={!isEnabled}
spellCheck
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
<div className='flex justify-center w-full'> <div className='flex justify-center w-full'>
@ -283,7 +291,7 @@ function EditorConstituenta({
</div> </div>
</div> </div>
</form> </form>
{(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD && {(windowSize.width ?? 0) >= SIDELIST_HIDE_THRESHOLD ?
<div className='w-full mt-[2.25rem] border h-fit'> <div className='w-full mt-[2.25rem] border h-fit'>
<ViewSideConstituents <ViewSideConstituents
expression={expression} expression={expression}
@ -291,7 +299,7 @@ function EditorConstituenta({
activeID={activeID} activeID={activeID}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
/> />
</div>} </div> : null}
</div>); </div>);
} }

View File

@ -290,10 +290,9 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edit
], [colors]); ], [colors]);
return ( return (
<div <div tabIndex={-1}
className='w-full outline-none' className='w-full outline-none'
style={{minHeight: mainHeight}} style={{minHeight: mainHeight}}
tabIndex={-1}
onKeyDown={handleTableKey} onKeyDown={handleTableKey}
> >
<div className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-b items-center h-[2.2rem] select-none clr-app'> <div className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-b items-center h-[2.2rem] select-none clr-app'>
@ -313,11 +312,10 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edit
</div> </div>
<div className='w-full h-full text-sm'> <div className='w-full h-full text-sm'>
<DataTable <DataTable dense noFooter
data={schema?.items ?? []} data={schema?.items ?? []}
columns={columns} columns={columns}
headPosition='2.2rem' headPosition='2.2rem'
dense
onRowDoubleClicked={handleRowDoubleClicked} onRowDoubleClicked={handleRowDoubleClicked}
onRowClicked={handleRowClicked} onRowClicked={handleRowClicked}

View File

@ -20,7 +20,7 @@ import RSEditorControls from './elements/RSEditorControls';
import StatusBar from './elements/StatusBar'; import StatusBar from './elements/StatusBar';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id: string id?: string
activeCst?: IConstituenta activeCst?: IConstituenta
label: string label: string
disabled?: boolean disabled?: boolean
@ -141,12 +141,11 @@ function EditorRSExpression({
/> />
<div className='w-full max-h-[4.5rem] min-h-[4.5rem] flex'> <div className='w-full max-h-[4.5rem] min-h-[4.5rem] flex'>
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
<Button <Button noOutline
tooltip='Проверить формальное определение'
text='Проверить' text='Проверить'
tooltip='Проверить формальное определение'
dimensions='w-[6.75rem] min-h-[3rem] z-pop rounded-none' dimensions='w-[6.75rem] min-h-[3rem] z-pop rounded-none'
colors='clr-btn-default' colors='clr-btn-default'
noOutline
onClick={() => handleCheckExpression()} onClick={() => handleCheckExpression()}
/> />
<StatusBar <StatusBar
@ -156,19 +155,18 @@ function EditorRSExpression({
/> />
</div> </div>
<div className='w-full overflow-y-auto text-sm border rounded-none'> <div className='w-full overflow-y-auto text-sm border rounded-none'>
{ loading && <ConceptLoader size={6} />} {loading ? <ConceptLoader size={6} /> : null}
{ !loading && parseData && {(!loading && parseData) ?
<ParsingResult <ParsingResult
data={parseData} data={parseData}
disabled={disabled} disabled={disabled}
onShowError={onShowError} onShowError={onShowError}
/>} /> : null}
{ !loading && !parseData && {(!loading && !parseData) ?
<input <input disabled
disabled={true}
className='w-full px-2 py-1 text-base select-none h-fit clr-app' className='w-full px-2 py-1 text-base select-none h-fit clr-app'
placeholder='Результаты проверки выражения' placeholder='Результаты проверки выражения'
/>} /> : null}
</div> </div>
</div> </div>
</div>); </div>);

View File

@ -205,7 +205,9 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
</div> </div>
</div> </div>
</form> </form>
{schema && <Divider vertical />}
<Divider vertical />
<RSFormStats stats={schema?.stats}/> <RSFormStats stats={schema?.stats}/>
</div> </div>
</div>); </div>);

View File

@ -471,7 +471,10 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
</div> </div>
</div> </div>
<div className='w-full h-full overflow-auto outline-none' tabIndex={-1} onKeyDown={handleKeyDown}> <div tabIndex={-1}
className='w-full h-full overflow-auto outline-none'
onKeyDown={handleKeyDown}
>
<div <div
className='relative' className='relative'
style={{width: canvasWidth, height: canvasHeight}} style={{width: canvasWidth, height: canvasHeight}}

View File

@ -335,54 +335,54 @@ function RSTabs() {
return ( return (
<div className='w-full'> <div className='w-full'>
{ loading && <ConceptLoader /> } {loading ? <ConceptLoader /> : null}
{ error && <ProcessError error={error} />} {error ? <ProcessError error={error} /> : null}
{ schema && !loading && <> {(schema && !loading) ? <>
{showUpload && {showUpload ?
<DlgUploadRSForm <DlgUploadRSForm
hideWindow={() => setShowUpload(false)} hideWindow={() => setShowUpload(false)}
/>} /> : null}
{showClone && {showClone ?
<DlgCloneRSForm <DlgCloneRSForm
hideWindow={() => setShowClone(false)} hideWindow={() => setShowClone(false)}
/>} /> : null}
{showAST && {showAST ?
<DlgShowAST <DlgShowAST
expression={expression} expression={expression}
syntaxTree={syntaxTree} syntaxTree={syntaxTree}
hideWindow={() => setShowAST(false)} hideWindow={() => setShowAST(false)}
/>} /> : null}
{showCreateCst && {showCreateCst ?
<DlgCreateCst <DlgCreateCst
hideWindow={() => setShowCreateCst(false)} hideWindow={() => setShowCreateCst(false)}
onCreate={handleCreateCst} onCreate={handleCreateCst}
schema={schema} schema={schema}
initial={createInitialData} initial={createInitialData}
/>} /> : null}
{showRenameCst && {showRenameCst ?
<DlgRenameCst <DlgRenameCst
hideWindow={() => setShowRenameCst(false)} hideWindow={() => setShowRenameCst(false)}
onRename={handleRenameCst} onRename={handleRenameCst}
initial={renameInitialData!} initial={renameInitialData!}
/>} /> : null}
{showDeleteCst && {showDeleteCst ?
<DlgDeleteCst <DlgDeleteCst
hideWindow={() => setShowDeleteCst(false)} hideWindow={() => setShowDeleteCst(false)}
onDelete={handleDeleteCst} onDelete={handleDeleteCst}
selected={toBeDeleted} selected={toBeDeleted}
/>} /> : null}
{showEditTerm && {showEditTerm ?
<DlgEditWordForms <DlgEditWordForms
hideWindow={() => setShowEditTerm(false)} hideWindow={() => setShowEditTerm(false)}
onSave={handleSaveWordforms} onSave={handleSaveWordforms}
target={activeCst!} target={activeCst!}
/>} /> : null}
{showTemplates && {showTemplates ?
<DlgConstituentaTemplate <DlgConstituentaTemplate
schema={schema} schema={schema}
hideWindow={() => setShowTemplates(false)} hideWindow={() => setShowTemplates(false)}
onCreate={handleCreateCst} onCreate={handleCreateCst}
/>} /> : null}
<Tabs <Tabs
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={onSelectTab} onSelect={onSelectTab}
@ -465,7 +465,7 @@ function RSTabs() {
</TabPanel> </TabPanel>
</div> </div>
</Tabs> </Tabs>
</>} </> : null}
</div>); </div>);
} }

View File

@ -13,22 +13,23 @@ function ParsingResult({ data, disabled, onShowError }: ParsingResultProps) {
const warningsCount = data.errors.length - errorCount; const warningsCount = data.errors.length - errorCount;
return ( return (
<div className='px-2 py-1'> <div className='px-2 py-1'>
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p> <p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
{data.errors.map((error, index) => { {data.errors.map(
return ( (error, index) => {
<p return (
key={`error-${index}`} <p
className={`text-warning ${disabled ? '' : 'cursor-pointer'}`} key={`error-${index}`}
onClick={disabled ? undefined : () => onShowError(error)} className={`text-warning ${disabled ? '' : 'cursor-pointer'}`}
> onClick={disabled ? undefined : () => onShowError(error)}
<span className='mr-1 font-semibold underline'>{error.isCritical ? 'Ошибка' : 'Предупреждение'} {getRSErrorPrefix(error)}:</span> >
<span> {describeRSError(error)}</span> <span className='mr-1 font-semibold underline'>
</p> {error.isCritical ? 'Ошибка' : 'Предупреждение'} {`${getRSErrorPrefix(error)}:`}
); </span>
})} <span>{` ${describeRSError(error)}`}</span>
</div> </p>);
) })}
</div>);
} }
export default ParsingResult; export default ParsingResult;

View File

@ -11,81 +11,84 @@ function RSFormStats({ stats }: RSFormStatsProps) {
return null; return null;
} }
return ( return (
<div className='flex flex-col gap-1 px-4 py-2 mt-7 min-w-[16rem]'> <div className='flex flex-col gap-1 px-4 py-2 mt-7 min-w-[16rem]'>
<LabeledText id='count_all' <LabeledText id='count_all'
label='Всего конституент ' label='Всего конституент '
text={stats.count_all} text={stats.count_all}
/> />
<LabeledText id='count_errors' <LabeledText id='count_errors'
label='Ошибок ' label='Некорректных'
text={stats.count_errors} text={stats.count_errors}
/> />
{ stats.count_property > 0 && {stats.count_property !== 0 ?
<LabeledText id='count_property' <LabeledText id='count_property'
label='Только свойство ' label='Неразмерных'
text={stats.count_property} text={stats.count_property}
/>} /> : null}
{ stats.count_incalc > 0 && {stats.count_incalc !== 0 ?
<LabeledText id='count_incalc' <LabeledText id='count_incalc'
label='Невычислимы ' label='Невычислимых'
text={stats.count_incalc} text={stats.count_incalc}
/>} /> : null}
<Divider margins='my-2' />
<LabeledText id='count_termin' <Divider margins='my-2' />
label='Термины '
text={stats.count_termin} <LabeledText id='count_termin'
/> label='Термины'
<LabeledText id='count_definition' text={stats.count_termin}
label='Определения ' />
text={stats.count_definition} <LabeledText id='count_definition'
/> label='Определения'
<LabeledText id='count_convention' text={stats.count_definition}
label='Конвенции ' />
text={stats.count_convention} <LabeledText id='count_convention'
/> label='Конвенции'
<Divider margins='my-2' /> text={stats.count_convention}
{ stats.count_base > 0 && />
<LabeledText id='count_base'
label='Базисные множества ' <Divider margins='my-2' />
text={stats.count_base}
/>} {stats.count_base !== 0 ?
{ stats.count_constant > 0 && <LabeledText id='count_base'
<LabeledText id='count_constant' label='Базисные множества '
label='Константные множества ' text={stats.count_base}
text={stats.count_constant} /> : null}
/>} { stats.count_constant !== 0 ?
{ stats.count_structured > 0 && <LabeledText id='count_constant'
<LabeledText id='count_structured' label='Константные множества '
label='Родовые структуры ' text={stats.count_constant}
text={stats.count_structured} /> : null}
/>} {stats.count_structured !== 0 ?
{ stats.count_axiom > 0 && <LabeledText id='count_structured'
<LabeledText id='count_axiom' label='Родовые структуры '
label='Аксиомы ' text={stats.count_structured}
text={stats.count_axiom} /> : null}
/>} {stats.count_axiom !== 0 ?
{ stats.count_term > 0 && <LabeledText id='count_axiom'
<LabeledText id='count_term' label='Аксиомы '
label='Термы ' text={stats.count_axiom}
text={stats.count_term} /> : null}
/>} {stats.count_term !== 0 ?
{ stats.count_function > 0 && <LabeledText id='count_term'
<LabeledText id='count_function' label='Термы '
label='Терм-функции ' text={stats.count_term}
text={stats.count_function} /> : null}
/>} {stats.count_function !== 0 ?
{ stats.count_predicate > 0 && <LabeledText id='count_function'
<LabeledText id='count_predicate' label='Терм-функции '
label='Предикат-функции ' text={stats.count_function}
text={stats.count_predicate} /> : null}
/>} {stats.count_predicate !== 0 ?
{ stats.count_theorem > 0 && <LabeledText id='count_predicate'
<LabeledText id='count_theorem' label='Предикат-функции '
label='Теоремы ' text={stats.count_predicate}
text={stats.count_theorem} /> : null}
/>} {stats.count_theorem !== 0 ?
</div> <LabeledText id='count_theorem'
); label='Теоремы '
text={stats.count_theorem}
/> : null}
</div>);
} }
export default RSFormStats; export default RSFormStats;

View File

@ -9,17 +9,14 @@ interface RSLocalButtonProps {
function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps) { function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps) {
return ( return (
<button <button type='button' tabIndex={-1}
type='button' disabled={disabled}
disabled={disabled} title={tooltip}
onClick={() => onInsert(TokenID.ID_LOCAL, text)} className='w-[2rem] h-6 cursor-pointer disabled:cursor-default border rounded-none clr-hover clr-btn-clear'
title={tooltip} onClick={() => onInsert(TokenID.ID_LOCAL, text)}
tabIndex={-1} >
className='w-[2rem] h-6 cursor-pointer disabled:cursor-default border rounded-none clr-hover clr-btn-clear' {text}
> </button>);
{text}
</button>
);
} }
export default RSLocalButton; export default RSLocalButton;

View File

@ -72,17 +72,14 @@ function RSTabsMenu({
return ( return (
<div className='flex items-stretch h-full w-fit'> <div className='flex items-stretch h-full w-fit'>
<div ref={schemaMenu.ref}> <div ref={schemaMenu.ref}>
<Button <Button noBorder dense tabIndex={-1}
tooltip='Действия' tooltip='Действия'
icon={<MenuIcon color='text-controls' size={5}/>} icon={<MenuIcon color='text-controls' size={5}/>}
dimensions='h-full w-fit pl-2' dimensions='h-full w-fit pl-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
noBorder
dense
onClick={schemaMenu.toggle} onClick={schemaMenu.toggle}
tabIndex={-1}
/> />
{ schemaMenu.isActive && {schemaMenu.isActive ?
<Dropdown> <Dropdown>
<DropdownButton onClick={handleShare}> <DropdownButton onClick={handleShare}>
<div className='inline-flex items-center justify-start gap-2'> <div className='inline-flex items-center justify-start gap-2'>
@ -120,20 +117,17 @@ function RSTabsMenu({
<p>Создать новую схему</p> <p>Создать новую схему</p>
</span> </span>
</DropdownButton> </DropdownButton>
</Dropdown>} </Dropdown> : null}
</div> </div>
<div ref={editMenu.ref}> <div ref={editMenu.ref}>
<Button <Button dense noBorder tabIndex={-1}
tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')} tooltip={'измнение: ' + (isEditable ? '[доступно]' : '[запрещено]')}
dimensions='h-full w-fit' dimensions='h-full w-fit'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
icon={<EditIcon size={5} color={isEditable ? 'text-success' : 'text-warning'}/>} icon={<EditIcon size={5} color={isEditable ? 'text-success' : 'text-warning'}/>}
dense
noBorder
onClick={editMenu.toggle} onClick={editMenu.toggle}
tabIndex={-1}
/> />
{ editMenu.isActive && {editMenu.isActive ?
<Dropdown> <Dropdown>
<DropdownButton <DropdownButton
disabled={!user || !isClaimable} disabled={!user || !isClaimable}
@ -141,31 +135,33 @@ function RSTabsMenu({
tooltip={!user || !isClaimable ? 'Взять во владение можно общую изменяемую схему' : ''} tooltip={!user || !isClaimable ? 'Взять во владение можно общую изменяемую схему' : ''}
> >
<div className='flex items-center gap-2 pl-1'> <div className='flex items-center gap-2 pl-1'>
<span><OwnerIcon size={5} color={isOwned ? 'text-success' : 'text-controls'} /></span> <span>
<OwnerIcon size={5} color={isOwned ? 'text-success' : 'text-controls'} />
</span>
<p> <p>
{ isOwned && <b>Владелец схемы</b> } {isOwned ? <b>Владелец схемы</b> : null}
{ !isOwned && <b>Стать владельцем</b> } {!isOwned ? <b>Стать владельцем</b> : null}
</p> </p>
</div> </div>
</DropdownButton> </DropdownButton>
{(isOwned || user?.is_staff) && {(isOwned || user?.is_staff) ?
<DropdownCheckbox <DropdownCheckbox
value={isReadonly} value={isReadonly}
setValue={toggleReadonly} setValue={toggleReadonly}
label='Я — читатель!' label='Я — читатель!'
tooltip='Режим чтения' tooltip='Режим чтения'
/>} /> : null}
{user?.is_staff && {user?.is_staff ?
<DropdownCheckbox <DropdownCheckbox
value={isForceAdmin} value={isForceAdmin}
setValue={toggleForceAdmin} setValue={toggleForceAdmin}
label='Я — администратор!' label='Я — администратор!'
tooltip='Режим редактирования для администраторов' tooltip='Режим редактирования для администраторов'
/>} /> : null}
</Dropdown>} </Dropdown>: null}
</div> </div>
<div> <div>
<Button <Button dense noBorder tabIndex={-1}
tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')} tooltip={'отслеживание: ' + (isTracking ? '[включено]' : '[выключено]')}
disabled={processing} disabled={processing}
icon={isTracking icon={isTracking
@ -174,10 +170,7 @@ function RSTabsMenu({
} }
dimensions='h-full w-fit pr-2' dimensions='h-full w-fit pr-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
dense
noBorder
onClick={onToggleSubscribe} onClick={onToggleSubscribe}
tabIndex={-1}
/> />
</div> </div>
</div>); </div>);

View File

@ -11,17 +11,14 @@ function RSTokenButton({ token, disabled, onInsert }: RSTokenButtonProps) {
const label = labelToken(token); const label = labelToken(token);
const width = label.length > 3 ? 'w-[4.5rem]' : 'w-[2.25rem]'; const width = label.length > 3 ? 'w-[4.5rem]' : 'w-[2.25rem]';
return ( return (
<button <button type='button' tabIndex={-1}
type='button' disabled={disabled}
disabled={disabled} onClick={() => onInsert(token)}
onClick={() => onInsert(token)} title={describeToken(token)}
title={describeToken(token)} className={`px-1 cursor-pointer disabled:cursor-default border rounded-none h-6 ${width} outline-none clr-hover clr-btn-clear`}
tabIndex={-1} >
className={`px-1 cursor-pointer disabled:cursor-default border rounded-none h-6 ${width} outline-none clr-hover clr-btn-clear`} {label ? <span className='whitespace-nowrap'>{label}</span> : null}
> </button>);
{label && <span className='whitespace-nowrap'>{label}</span>}
</button>
);
} }
export default RSTokenButton; export default RSTokenButton;

View File

@ -206,18 +206,16 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
/> />
<div className='flex'> <div className='flex'>
<div ref={matchModeMenu.ref}> <div ref={matchModeMenu.ref}>
<SelectorButton <SelectorButton transparent tabIndex={-1}
tooltip='Настройка атрибутов для фильтрации' tooltip='Настройка атрибутов для фильтрации'
dimensions='w-fit h-full' dimensions='w-fit h-full'
transparent
icon={<FilterIcon size={5} />} icon={<FilterIcon size={5} />}
text={labelCstMathchMode(filterMatch)} text={labelCstMathchMode(filterMatch)}
tabIndex={-1}
onClick={matchModeMenu.toggle} onClick={matchModeMenu.toggle}
/> />
{ matchModeMenu.isActive && { matchModeMenu.isActive &&
<Dropdown stretchLeft> <Dropdown stretchLeft>
{ Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map( {Object.values(CstMatchMode).filter(value => !isNaN(Number(value))).map(
(value, index) => { (value, index) => {
const matchMode = value as CstMatchMode; const matchMode = value as CstMatchMode;
return ( return (
@ -232,18 +230,16 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
</div> </div>
<div ref={sourceMenu.ref}> <div ref={sourceMenu.ref}>
<SelectorButton <SelectorButton transparent tabIndex={-1}
tooltip='Настройка фильтрации по графу термов' tooltip='Настройка фильтрации по графу термов'
dimensions='w-fit h-full' dimensions='w-fit h-full'
transparent
icon={<CogIcon size={4} />} icon={<CogIcon size={4} />}
text={labelCstSource(filterSource)} text={labelCstSource(filterSource)}
tabIndex={-1}
onClick={sourceMenu.toggle} onClick={sourceMenu.toggle}
/> />
{ sourceMenu.isActive && {sourceMenu.isActive ?
<Dropdown stretchLeft> <Dropdown stretchLeft>
{ Object.values(CstSource).filter(value => !isNaN(Number(value))).map( {Object.values(CstSource).filter(value => !isNaN(Number(value))).map(
(value, index) => { (value, index) => {
const source = value as CstSource; const source = value as CstSource;
return ( return (
@ -254,18 +250,16 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
<p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p> <p><span className='font-semibold'>{labelCstSource(source)}:</span> {describeCstSource(source)}</p>
</DropdownButton>); </DropdownButton>);
})} })}
</Dropdown>} </Dropdown> : null}
</div> </div>
</div> </div>
</div> </div>
<div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}> <div className='overflow-y-auto text-sm overscroll-none' style={{maxHeight : `${maxHeight}`}}>
<DataTable <DataTable dense noFooter
data={filteredData} data={filteredData}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
headPosition='0' headPosition='0'
dense
noFooter
enableHiding enableHiding
columnVisibility={columnVisibility} columnVisibility={columnVisibility}

View File

@ -54,69 +54,68 @@ function RegisterPage() {
} }
return ( return (
<div className='flex justify-center w-full py-2'> <div className='flex justify-center w-full py-2'>
{ user && {user ? <b>{`Вы вошли в систему как ${user.username}`}</b> : null}
<b>{`Вы вошли в систему как ${user.username}`}</b>} {!user ?
{ !user && <Form
<Form title='Регистрация'
title='Регистрация' onSubmit={handleSubmit}
onSubmit={handleSubmit} dimensions='w-[24rem]'
dimensions='w-[24rem]' >
> <TextInput id='username' required
<TextInput id='username' label='Имя пользователя' type='text' label='Имя пользователя'
required value={username}
value={username} onChange={event => setUsername(event.target.value)}
onChange={event => setUsername(event.target.value)} />
/> <TextInput id='password' type='password' required
<TextInput id='password' label='Пароль' type='password' label='Пароль'
required value={password}
value={password} onChange={event => setPassword(event.target.value)}
onChange={event => setPassword(event.target.value)} />
/> <TextInput id='password2' required type='password'
<TextInput id='password2' label='Повторите пароль' type='password' label='Повторите пароль'
required value={password2}
value={password2} onChange={event => setPassword2(event.target.value)}
onChange={event => setPassword2(event.target.value)} />
/> <div className='text-sm'>
<div className='text-sm'> <p>- используйте уникальный пароль</p>
<p>- используйте уникальный пароль</p> <p>- портал функционирует в тестовом режиме</p>
<p>- портал функционирует в тестовом режиме</p> <p className='font-semibold text-warning'>- безопасность информации не гарантируется</p>
<p className='font-semibold text-warning'>- безопасность информации не гарантируется</p> {/* <p>- минимум 8 символов</p>
{/* <p>- минимум 8 символов</p> <p>- большие, маленькие буквы, цифры</p>
<p>- большие, маленькие буквы, цифры</p> <p>- минимум 1 спец. символ</p> */}
<p>- минимум 1 спец. символ</p> */} </div>
</div> <TextInput id='email' required
<TextInput id='email' label='email' type='text' label='email'
required value={email}
value={email} onChange={event => setEmail(event.target.value)}
onChange={event => setEmail(event.target.value)} />
/> <TextInput id='first_name'
<TextInput id='first_name' label='Имя' type='text' label='Имя'
value={firstName} value={firstName}
onChange={event => setFirstName(event.target.value)} onChange={event => setFirstName(event.target.value)}
/> />
<TextInput id='last_name' label='Фамилия' type='text' <TextInput id='last_name'
value={lastName} label='Фамилия'
onChange={event => setLastName(event.target.value)} value={lastName}
/> onChange={event => setLastName(event.target.value)}
/>
<div className='flex items-center justify-center w-full gap-4 my-4'> <div className='flex items-center justify-center w-full gap-4 my-4'>
<SubmitButton <SubmitButton
text='Регистрировать' text='Регистрировать'
loading={loading} dimensions='min-w-[10rem]'
dimensions='min-w-[10rem]' loading={loading}
/> />
<Button <Button
text='Отмена' text='Отмена'
onClick={() => handleCancel()} dimensions='min-w-[10rem]'
dimensions='min-w-[10rem]' onClick={() => handleCancel()}
/> />
</div> </div>
{ error && <BackendError error={error} />} {error ? <BackendError error={error} /> : null}
</Form> </Form> : null}
} </div>);
</div>
);
} }
export default RegisterPage; export default RegisterPage;

View File

@ -31,7 +31,15 @@ function EditorPassword() {
const passwordColor = useMemo( const passwordColor = useMemo(
() => { () => {
return !!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'clr-warning' : 'clr-input'; if (
!!newPassword &&
!!newPasswordRepeat &&
newPassword !== newPasswordRepeat
) {
return 'clr-warning';
} else {
return 'clr-input';
}
}, [newPassword, newPasswordRepeat]); }, [newPassword, newPasswordRepeat]);
const canSubmit = useMemo( const canSubmit = useMemo(
@ -60,46 +68,44 @@ function EditorPassword() {
}, [newPassword, oldPassword, newPasswordRepeat, setError]); }, [newPassword, oldPassword, newPasswordRepeat, setError]);
return ( return (
<div className='flex py-2 border-l-2 max-w-[14rem]'> <div className='flex py-2 border-l-2 max-w-[14rem]'>
<form onSubmit={handleSubmit} className='flex flex-col justify-between px-6'> <form
<div className='flex flex-col gap-3'> className='flex flex-col justify-between px-6'
<TextInput id='old_password' onSubmit={handleSubmit}
type='password' >
label='Старый пароль' <div className='flex flex-col gap-3'>
allowEnter <TextInput id='old_password' type='password' allowEnter
value={oldPassword} label='Старый пароль'
onChange={event => setOldPassword(event.target.value)} value={oldPassword}
/> onChange={event => setOldPassword(event.target.value)}
<TextInput id='new_password' type='password' />
colors={passwordColor} <TextInput id='new_password' type='password' allowEnter
label='Новый пароль' label='Новый пароль'
allowEnter colors={passwordColor}
value={newPassword} value={newPassword}
onChange={event => { onChange={event => {
setNewPassword(event.target.value); setNewPassword(event.target.value);
}} }}
/> />
<TextInput id='new_password_repeat' type='password' <TextInput id='new_password_repeat' type='password' allowEnter
colors={passwordColor} label='Повторите новый'
label='Повторите новый' colors={passwordColor}
allowEnter value={newPasswordRepeat}
value={newPasswordRepeat} onChange={event => {
onChange={event => { setNewPasswordRepeat(event.target.value);
setNewPasswordRepeat(event.target.value); }}
}} />
/> </div>
</div> {error ? <ProcessError error={error} /> : null}
{ error && <ProcessError error={error} />} <div className='flex justify-center w-full'>
<div className='flex justify-center w-full'> <SubmitButton
<SubmitButton text='Сменить пароль'
disabled={!canSubmit} disabled={!canSubmit}
loading={loading} loading={loading}
text='Сменить пароль' />
/> </div>
</div> </form>
</form> </div>);
</div>
)
} }
export default EditorPassword; export default EditorPassword;

View File

@ -50,42 +50,37 @@ function EditorProfile() {
} }
return ( return (
<form onSubmit={handleSubmit} className='px-6 py-2 flex flex-col gap-8 min-w-[18rem]'> <form onSubmit={handleSubmit} className='px-6 py-2 flex flex-col gap-8 min-w-[18rem]'>
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<TextInput id='username' <TextInput id='username' disabled
label='Логин' label='Логин'
tooltip='Логин изменить нельзя' tooltip='Логин изменить нельзя'
disabled value={username}
value={username} />
/> <TextInput id='first_name' allowEnter
<TextInput id='first_name' label='Имя'
label='Имя' value={first_name}
value={first_name} onChange={event => setFirstName(event.target.value)}
allowEnter />
onChange={event => setFirstName(event.target.value)} <TextInput id='last_name' allowEnter
/> label='Фамилия'
<TextInput id='last_name' value={last_name}
label='Фамилия' onChange={event => setLastName(event.target.value)}
value={last_name} />
allowEnter <TextInput id='email' allowEnter
onChange={event => setLastName(event.target.value)} label='Электронная почта'
/> value={email}
<TextInput id='email' onChange={event => setEmail(event.target.value)}
label='Электронная почта' />
allowEnter </div>
value={email} <div className='flex justify-center w-full'>
onChange={event => setEmail(event.target.value)} <SubmitButton
/> text='Сохранить данные'
</div> loading={processing}
<div className='flex justify-center w-full'> disabled={!isModified}
<SubmitButton />
text='Сохранить данные' </div>
loading={processing} </form>);
disabled={!isModified}
/>
</div>
</form>
)
} }
export default EditorProfile; export default EditorProfile;

View File

@ -24,39 +24,37 @@ function UserTabs() {
}, [auth, items]); }, [auth, items]);
return ( return (
<div className='w-full'> <div className='w-full'>
{ loading && <ConceptLoader /> } {loading ? <ConceptLoader /> : null}
{ error && <BackendError error={error} />} {error ? <BackendError error={error} /> : null}
{ user && {user ?
<div className='flex justify-center gap-2 py-2'> <div className='flex justify-center gap-2 py-2'>
<div className='flex flex-col gap-2 min-w-max'> <div className='flex flex-col gap-2 min-w-max'>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 right-0 mt-2'> <div className='absolute top-0 right-0 mt-2'>
<MiniButton <MiniButton
tooltip='Показать/Скрыть список отслеживаний' tooltip='Показать/Скрыть список отслеживаний'
icon={showSubs icon={showSubs
? <SubscribedIcon color='text-primary' size={5}/> ? <SubscribedIcon color='text-primary' size={5}/>
: <NotSubscribedIcon color='text-primary' size={5}/> : <NotSubscribedIcon color='text-primary' size={5}/>
} }
onClick={() => setShowSubs(prev => !prev)} onClick={() => setShowSubs(prev => !prev)}
/> />
</div>
</div> </div>
<h1>Учетные данные пользователя</h1>
<div className='flex justify-center py-2 max-w-fit'>
<EditorProfile />
<EditorPassword />
</div>
</div> </div>
{subscriptions.length > 0 && showSubs && <h1>Учетные данные пользователя</h1>
<div className='flex flex-col w-full gap-6 pl-4'> <div className='flex justify-center py-2 max-w-fit'>
<h1>Отслеживаемые схемы</h1> <EditorProfile />
<ViewSubscriptions items={subscriptions} /> <EditorPassword />
</div>} </div>
</div> </div>
} {(subscriptions.length > 0 && showSubs) ?
</div> <div className='flex flex-col w-full gap-6 pl-4'>
); <h1>Отслеживаемые схемы</h1>
<ViewSubscriptions items={subscriptions} />
</div> : null}
</div> : null}
</div>);
} }
export default UserTabs; export default UserTabs;

View File

@ -47,26 +47,22 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
], [intl]); ], [intl]);
return ( return (
<div className='max-h-[23.8rem] overflow-auto text-sm border w-fit'> <div className='max-h-[23.8rem] overflow-auto text-sm border w-fit'>
<DataTable <DataTable dense noFooter
columns={columns} columns={columns}
data={items} data={items}
headPosition='0' headPosition='0'
dense
enableSorting enableSorting
initialSorting={{ initialSorting={{
id: 'time_update', id: 'time_update',
desc: true desc: true
}} }}
noDataComponent={ noDataComponent={<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>}
<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>
}
onRowClicked={openRSForm} onRowClicked={openRSForm}
/> />
</div> </div>);
)
} }
export default ViewSubscriptions; export default ViewSubscriptions;

View File

@ -66,6 +66,7 @@ export const globalIDs = {
* Prefixes for generating unique keys for lists. * Prefixes for generating unique keys for lists.
*/ */
export const prefixes = { export const prefixes = {
page_size: 'page-size-',
cst_list: 'cst-list-', cst_list: 'cst-list-',
cst_hidden_list: 'cst-hidden-list-', cst_hidden_list: 'cst-hidden-list-',
cst_modal_list: 'cst-modal-list-', cst_modal_list: 'cst-modal-list-',