UI components refactoring and DOM optimization

This commit is contained in:
IRBorisov 2023-12-05 01:22:44 +03:00
parent 75da54cbd3
commit c1a994bc0c
87 changed files with 915 additions and 912 deletions

View File

@ -61,8 +61,7 @@ function BackendError({ error }: BackendErrorProps) {
return ( return (
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text text-warning'> <div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text text-warning'>
{DescribeError(error)} {DescribeError(error)}
</div> </div>);
);
} }
export default BackendError; export default BackendError;

View File

@ -1,4 +1,4 @@
import { IColorsProps, IControlProps } from '../commonInterfaces' import { IColorsProps, IControlProps } from '../commonInterfaces';
interface ButtonProps interface ButtonProps
extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'| 'type'> { extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'| 'type'> {

View File

@ -14,10 +14,9 @@ function ConceptSearch({ value, onChange, dense }: ConceptSearchProps) {
<div className='absolute inset-y-0 flex items-center pl-3 pointer-events-none text-controls'> <div className='absolute inset-y-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon /> <MagnifyingGlassIcon />
</div> </div>
<TextInput <TextInput noOutline
dimensions={`w-full pl-10 ${borderClass} rounded`}
placeholder='Поиск' placeholder='Поиск'
noOutline dimensions={`w-full pl-10 ${borderClass}`}
noBorder={dense} noBorder={dense}
value={value} value={value}
onChange={event => (onChange ? onChange(event.target.value) : undefined)} onChange={event => (onChange ? onChange(event.target.value) : undefined)}

View File

@ -10,8 +10,7 @@ function Dropdown({ children, dimensions = 'w-fit', stretchLeft }: DropdownProps
<div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-3 z-modal-tooltip flex flex-col items-stretch justify-start origin-top-right border rounded-md shadow-lg clr-input ${dimensions}`}> <div className={`absolute ${stretchLeft ? 'right-0' : 'left-0'} mt-3 z-modal-tooltip flex flex-col items-stretch justify-start origin-top-right border rounded-md shadow-lg clr-input ${dimensions}`}>
{children} {children}
</div> </div>
</div> </div>);
);
} }
export default Dropdown; export default Dropdown;

View File

@ -16,8 +16,7 @@ function DropdownButton({ tooltip, onClick, disabled, children }: DropdownButton
className={`px-3 py-1 text-left overflow-ellipsis whitespace-nowrap ${behavior} ${text}`} className={`px-3 py-1 text-left overflow-ellipsis whitespace-nowrap ${behavior} ${text}`}
> >
{children} {children}
</button> </button>);
);
} }
export default DropdownButton; export default DropdownButton;

View File

@ -21,8 +21,7 @@ function DropdownCheckbox({ tooltip, setValue, disabled, ...restProps }: Dropdow
setValue={setValue} setValue={setValue}
{...restProps} {...restProps}
/> />
</div> </div>);
);
} }
export default DropdownCheckbox; export default DropdownCheckbox;

View File

@ -13,18 +13,16 @@ function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
className='relative' className='relative'
style={{height: 0, paddingBottom: `${pxHeight}px`, paddingLeft: `${pxWidth}px`}} style={{height: 0, paddingBottom: `${pxHeight}px`, paddingLeft: `${pxWidth}px`}}
> >
<iframe <iframe allowFullScreen
title='Встроенное видео Youtube'
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
className='absolute top-0 left-0 border' className='absolute top-0 left-0 border'
style={{minHeight: `${pxHeight}px`, minWidth: `${pxWidth}px`}} style={{minHeight: `${pxHeight}px`, minWidth: `${pxWidth}px`}}
width={`${pxWidth}px`} width={`${pxWidth}px`}
height={`${pxHeight}px`} height={`${pxHeight}px`}
src={`https://www.youtube.com/embed/${videoID}`} src={`https://www.youtube.com/embed/${videoID}`}
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
allowFullScreen
title='Встроенное видео Youtube'
/> />
</div> </div>);
);
} }
export default EmbedYoutube; export default EmbedYoutube;

View File

@ -55,8 +55,7 @@ function FileInput({
<Label <Label
text={fileName} text={fileName}
/> />
</div> </div>);
);
} }
export default FileInput; export default FileInput;

View File

@ -1,20 +1,24 @@
interface FormProps { interface FormProps {
title?: string title?: string
className?: string
dimensions?: string dimensions?: string
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
children: React.ReactNode children: React.ReactNode
} }
function Form({ title, onSubmit, dimensions = 'max-w-xs', children }: FormProps) { function Form({
title, className, onSubmit,
dimensions='max-w-xs',
children
}: FormProps) {
return ( return (
<form <form
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} ${className}`}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{ title ? <h1 className='text-xl whitespace-nowrap'>{title}</h1> : null } { title ? <h1 className='text-xl whitespace-nowrap'>{title}</h1> : null }
{children} {children}
</form> </form>);
);
} }
export default Form; export default Form;

View File

@ -14,8 +14,7 @@ function Label({ text, tooltip, className, ...restProps }: LabelProps) {
{...restProps} {...restProps}
> >
{text} {text}
</label> </label>);
);
} }
export default Label; export default Label;

View File

@ -15,13 +15,10 @@ function LabeledText({ id, label, text, tooltip }: LabeledTextProps) {
> >
{label} {label}
</label> </label>
<span <span id={id}>
id={id}
>
{text} {text}
</span> </span>
</div> </div>);
);
} }
export default LabeledText; export default LabeledText;

View File

@ -19,8 +19,7 @@ function MiniButton({
{...restProps} {...restProps}
> >
{icon} {icon}
</button> </button>);
);
} }
export default MiniButton; export default MiniButton;

View File

@ -4,6 +4,7 @@ import useEscapeKey from '../../hooks/useEscapeKey';
import { CrossIcon } from '../Icons'; import { CrossIcon } from '../Icons';
import Button from './Button'; import Button from './Button';
import MiniButton from './MiniButton'; import MiniButton from './MiniButton';
import Overlay from './Overlay';
export interface ModalProps { export interface ModalProps {
title?: string title?: string
@ -43,15 +44,13 @@ function Modal({
<div ref={ref} <div ref={ref}
className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-6 w-fit max-w-[calc(100vw-2rem)] 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 w-fit max-w-[calc(100vw-2rem)] h-fit z-modal clr-app border shadow-md'
> >
<div className='relative'> <Overlay position='right-[-1rem] top-2' className='text-disabled'>
<div className='absolute right-[-1rem] top-2 text-disabled'>
<MiniButton <MiniButton
tooltip='Закрыть диалоговое окно [ESC]' tooltip='Закрыть диалоговое окно [ESC]'
icon={<CrossIcon size={5}/>} icon={<CrossIcon size={5}/>}
onClick={handleCancel} onClick={handleCancel}
/> />
</div> </Overlay>
</div>
{title ? <h1 className='my-2 text-lg select-none'>{title}</h1> : null} {title ? <h1 className='my-2 text-lg select-none'>{title}</h1> : null}

View File

@ -0,0 +1,21 @@
interface OverlayProps {
children: React.ReactNode
position?: string
className?: string
layer?: string
}
function Overlay({
children, className,
position='top-0 right-0',
layer='z-pop'
}: OverlayProps) {
return (
<div className='relative'>
<div className={`absolute ${className} ${position} ${layer}`}>
{children}
</div>
</div>);
}
export default Overlay;

View File

@ -66,8 +66,7 @@ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>
menuPortalTarget={!noPortal ? document.body : null} menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles} styles={adjustedStyles}
{...restProps} {...restProps}
/> />);
);
} }
export default SelectMulti; export default SelectMulti;

View File

@ -61,8 +61,7 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
menuPortalTarget={!noPortal ? document.body : null} menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles} styles={adjustedStyles}
{...restProps} {...restProps}
/> />);
);
} }
export default SelectSingle; export default SelectSingle;

View File

@ -28,8 +28,7 @@ function SelectorButton({
> >
{icon ? icon : null} {icon ? icon : null}
{text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null} {text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
</button> </button>);
);
} }
export default SelectorButton; export default SelectorButton;

View File

@ -14,7 +14,7 @@ function SubmitButton({
return ( return (
<button type='submit' <button type='submit'
title={tooltip} title={tooltip}
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-3 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border clr-btn-primary ${dimensions} ${loading ? ' cursor-progress' : ''}`}
disabled={disabled ?? loading} disabled={disabled ?? loading}
{...restProps} {...restProps}
> >

View File

@ -31,8 +31,7 @@ function TextArea({
required={required} required={required}
{...restProps} {...restProps}
/> />
</div> </div>);
);
} }
export default TextArea; export default TextArea;

View File

@ -34,8 +34,7 @@ function TextInput({
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}`}
{...restProps} {...restProps}
/> />
</div> </div>);
);
} }
export default TextInput; export default TextInput;

View File

@ -2,15 +2,33 @@ import { Link } from 'react-router-dom';
interface TextURLProps { interface TextURLProps {
text: string text: string
href: string tooltip?: string
href?: string
onClick?: () => void
} }
function TextURL({ text, href }: TextURLProps) { function TextURL({ text, href, tooltip, onClick }: TextURLProps) {
if (href) {
return ( return (
<Link className='hover:underline text-url' to={href}> <Link
className='cursor-pointer hover:underline text-url'
title={tooltip}
to={href}
>
{text} {text}
</Link> </Link>
); );
} else if (onClick) {
return (
<span
className='cursor-pointer hover:underline text-url'
onClick={onClick}
>
{text}
</span>);
} else {
return null;
}
} }
export default TextURL; export default TextURL;

View File

@ -64,8 +64,7 @@ function Tristate({
text={label} text={label}
htmlFor={id} htmlFor={id}
/> : null} /> : null}
</button> </button>);
);
} }
export default Tristate; export default Tristate;

View File

@ -11,8 +11,7 @@ function ToasterThemed(props: ToasterThemedProps) {
<ToastContainer <ToastContainer
theme={ darkMode ? 'dark' : 'light'} theme={ darkMode ? 'dark' : 'light'}
{...props} {...props}
/> />);
);
} }
export default ToasterThemed; export default ToasterThemed;

View File

@ -73,7 +73,7 @@ export default function DataTable<TData extends RowData>({
paginationOptions=[10, 20, 30, 40, 50], paginationOptions=[10, 20, 30, 40, 50],
onChangePaginationOption, onChangePaginationOption,
...options ...restProps
}: DataTableProps<TData>) { }: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []); const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
@ -97,16 +97,16 @@ export default function DataTable<TData extends RowData>({
onPaginationChange: enablePagination ? setPagination : undefined, onPaginationChange: enablePagination ? setPagination : undefined,
onSortingChange: enableSorting ? setSorting : undefined, onSortingChange: enableSorting ? setSorting : undefined,
enableMultiRowSelection: enableRowSelection, enableMultiRowSelection: enableRowSelection,
...options ...restProps
}); });
const isEmpty = tableImpl.getRowModel().rows.length === 0; const isEmpty = tableImpl.getRowModel().rows.length === 0;
function getRowStyles(row: Row<TData>) { function getRowStyles(row: Row<TData>) {
return {...conditionalRowStyles! return ({...conditionalRowStyles!
.filter(item => item.when(row.original)) .filter(item => item.when(row.original))
.reduce((prev, item) => ({...prev, ...item.style}), {}) .reduce((prev, item) => ({...prev, ...item.style}), {})
}; });
} }
return ( return (

View File

@ -46,7 +46,7 @@ function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOp
> >
<GotoPrevIcon /> <GotoPrevIcon />
</button> </button>
<input type='text' <input
title='Номер страницы. Выделите для ручного ввода' title='Номер страницы. Выделите для ручного ввода'
className='w-6 text-center clr-app' className='w-6 text-center clr-app'
value={table.getState().pagination.pageIndex + 1} value={table.getState().pagination.pageIndex + 1}

View File

@ -17,8 +17,7 @@ function Footer() {
</div> </div>
</div> </div>
</footer > </footer>);
);
} }
export default Footer; export default Footer;

View File

@ -14,8 +14,7 @@ function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
className='max-w-[25rem] min-w-[25rem]' className='max-w-[25rem] min-w-[25rem]'
> >
<InfoConstituenta data={data} /> <InfoConstituenta data={data} />
</ConceptTooltip> </ConceptTooltip>);
);
} }
export default ConstituentaTooltip; export default ConstituentaTooltip;

View File

@ -0,0 +1,38 @@
import { HelpTopic } from '../../models/miscelanious';
import ConceptTooltip from '../Common/ConceptTooltip';
import TextURL from '../Common/TextURL';
import { HelpIcon } from '../Icons';
import InfoTopic from './InfoTopic';
interface HelpButtonProps {
topic: HelpTopic
offset?: number
dimensions?: string
}
function HelpButton({ topic, offset, dimensions }: HelpButtonProps) {
return (
<>
<div
id={`help-${topic}`}
className='px-1 py-1'
>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip clickable
anchorSelect={`#help-${topic}`}
layer='z-modal-tooltip'
className={dimensions}
offset={offset}
>
<div className='relative'>
<div className='absolute right-0 text-sm top-[0.4rem]'>
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
</div>
</div>
<InfoTopic topic={topic} />
</ConceptTooltip>
</>);
}
export default HelpButton;

View File

@ -2,7 +2,7 @@
function HelpTerminologyControl() { function HelpTerminologyControl() {
return ( return (
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<h1>Терминологизация: Контроль терминологии</h1> <h1>Терминологизация</h1>
<p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p> <p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p>
<p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>связывание слов.</i></p> <p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>связывание слов.</i></p>
<p>При отсылке к термину указывается параметры словоформы так, обеспечивающие корректное согласование слов.</p> <p>При отсылке к термину указывается параметры словоформы так, обеспечивающие корректное согласование слов.</p>

View File

@ -0,0 +1,33 @@
import { HelpTopic } from '../../models/miscelanious';
import HelpAPI from './HelpAPI';
import HelpConstituenta from './HelpConstituenta';
import HelpExteor from './HelpExteor';
import HelpLibrary from './HelpLibrary';
import HelpMain from './HelpMain';
import HelpRSFormItems from './HelpRSFormItems';
import HelpRSFormMeta from './HelpRSFormMeta';
import HelpRSLang from './HelpRSLang';
import HelpRSTemplates from './HelpRSTemplates';
import HelpTermGraph from './HelpTermGraph';
import HelpTerminologyControl from './HelpTerminologyControl';
interface InfoTopicProps {
topic: HelpTopic
}
function InfoTopic({ topic }: InfoTopicProps) {
if (topic === HelpTopic.MAIN) return <HelpMain />;
if (topic === HelpTopic.LIBRARY) return <HelpLibrary />;
if (topic === HelpTopic.RSFORM) return <HelpRSFormMeta />;
if (topic === HelpTopic.CSTLIST) return <HelpRSFormItems />;
if (topic === HelpTopic.CONSTITUENTA) return <HelpConstituenta />;
if (topic === HelpTopic.GRAPH_TERM) return <HelpTermGraph />;
if (topic === HelpTopic.RSTEMPLATES) return <HelpRSTemplates />;
if (topic === HelpTopic.RSLANG) return <HelpRSLang />;
if (topic === HelpTopic.TERM_CONTROL) return <HelpTerminologyControl />;
if (topic === HelpTopic.EXTEOR) return <HelpExteor />;
if (topic === HelpTopic.API) return <HelpAPI />;
return null;
}
export default InfoTopic;

View File

@ -14,7 +14,7 @@ export interface IconProps {
} }
function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) { function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) {
const width = `${size * 1 / 4}rem` const width = `${size * 1 / 4}rem`;
return ( return (
<svg <svg
width={width} width={width}
@ -25,8 +25,7 @@ function IconSVG({ viewbox, size = 6, color, props, children }: IconSVGProps) {
{...props} {...props}
> >
{children} {children}
</svg> </svg>);
);
} }
export function MagnifyingGlassIcon({ size, ...props }: IconProps) { export function MagnifyingGlassIcon({ size, ...props }: IconProps) {

View File

@ -29,7 +29,6 @@ function Logo() {
className='max-h-[1.6rem] min-w-[2.2rem]' className='max-h-[1.6rem] min-w-[2.2rem]'
/> : null} /> : null}
</Link> </Link>);
);
} }
export default Logo; export default Logo;

View File

@ -3,6 +3,7 @@ import { useConceptTheme } from '../../context/ThemeContext';
import { EducationIcon, LibraryIcon, PlusIcon } from '../Icons'; import { EducationIcon, LibraryIcon, PlusIcon } from '../Icons';
import Logo from './Logo' import Logo from './Logo'
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
import ToggleNavigationButton from './ToggleNavigationButton';
import UserMenu from './UserMenu'; import UserMenu from './UserMenu';
function Navigation () { function Navigation () {
@ -15,22 +16,7 @@ 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 ? <ToggleNavigationButton noNavigation={noNavigation} toggleNoNavigation={toggleNoNavigation} />
<button type='button' tabIndex={-1}
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'
onClick={toggleNoNavigation}
>
{''}
</button> : null}
{!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 ? {!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'>
@ -58,8 +44,7 @@ function Navigation () {
<UserMenu /> <UserMenu />
</div> </div>
</div> : null} </div> : null}
</nav> </nav>);
);
} }
export default Navigation; export default Navigation;

View File

@ -15,8 +15,7 @@ function NavigationButton({ id, icon, description, onClick, text }: NavigationBu
> >
{icon ? <span>{icon}</span> : null} {icon ? <span>{icon}</span> : null}
{text ? <span className='font-semibold'>{text}</span> : null} {text ? <span className='font-semibold'>{text}</span> : null}
</button> </button>);
);
} }
export default NavigationButton; export default NavigationButton;

View File

@ -0,0 +1,28 @@
interface ToggleNavigationButtonProps {
noNavigation?: boolean
toggleNoNavigation: () => void
}
function ToggleNavigationButton({ noNavigation, toggleNoNavigation }: ToggleNavigationButtonProps) {
if (noNavigation) {
return (
<button type='button' tabIndex={-1}
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'
onClick={toggleNoNavigation}
>
{''}
</button>);
} else {
return (
<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>);
}
}
export default ToggleNavigationButton;

View File

@ -41,8 +41,7 @@ function UserDropdown({ hideDropdown }: UserDropdownProps) {
<DropdownButton onClick={logoutAndRedirect}> <DropdownButton onClick={logoutAndRedirect}>
<b>Выйти...</b> <b>Выйти...</b>
</DropdownButton> </DropdownButton>
</Dropdown> </Dropdown>);
);
} }
export default UserDropdown; export default UserDropdown;

View File

@ -32,8 +32,7 @@ function UserMenu() {
<UserDropdown <UserDropdown
hideDropdown={() => menu.hide()} hideDropdown={() => menu.hide()}
/> : null} /> : null}
</div> </div>);
);
} }
export default UserMenu; export default UserMenu;

View File

@ -118,12 +118,11 @@ function RSInput({
}, [thisRef]); }, [thisRef]);
return ( return (
<div className={`flex flex-col ${dimensions} ${cursor}`}> <div className={`flex flex-col gap-2 ${dimensions} ${cursor}`}>
{label ? {label ?
<Label <Label
text={label} text={label}
htmlFor={id} htmlFor={id}
className='mb-2'
/> : null} /> : null}
<CodeMirror id={id} <CodeMirror id={id}
ref={thisRef} ref={thisRef}
@ -136,8 +135,7 @@ function RSInput({
onKeyDown={handleInput} onKeyDown={handleInput}
{...restProps} {...restProps}
/> />
</div> </div>);
);
} }
export default RSInput; export default RSInput;

View File

@ -1,4 +1,4 @@
import {LRLanguage} from '@codemirror/language' import {LRLanguage} from '@codemirror/language';
import { parser } from './parser'; import { parser } from './parser';
import { Function, Global, Predicate } from './parser.terms'; import { Function, Global, Predicate } from './parser.terms';

View File

@ -219,7 +219,6 @@ function RefsInput({
onKeyDown={handleInput} onKeyDown={handleInput}
onFocus={handleFocusIn} onFocus={handleFocusIn}
onBlur={handleFocusOut} onBlur={handleFocusOut}
spellCheck
// spellCheck= // TODO: figure out while automatic spellcheck doesnt work or implement with extension // spellCheck= // TODO: figure out while automatic spellcheck doesnt work or implement with extension
{...restProps} {...restProps}
/> />

View File

@ -1,4 +1,4 @@
import {LRLanguage} from '@codemirror/language' import {LRLanguage} from '@codemirror/language';
import { parser } from './parser'; import { parser } from './parser';
import { RefEntity, RefSyntactic } from './parser.terms'; import { RefEntity, RefSyntactic } from './parser.terms';

View File

@ -13,7 +13,8 @@ interface ConstituentaBadgeProps {
} }
function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) { function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) {
return (<div className='w-fit'> return (
<div className='w-fit'>
<div <div
id={`${prefixID}${value.alias}`} id={`${prefixID}${value.alias}`}
className='min-w-[3.1rem] max-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap' className='min-w-[3.1rem] max-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap'

View File

@ -35,8 +35,8 @@ function ConstituentaPicker({
onSelectValue onSelectValue
} : ConstituentaPickerProps) { } : ConstituentaPickerProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [ filteredData, setFilteredData ] = useState<IConstituenta[]>([]); const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const [ filterText, setFilterText ] = useState(''); const [filterText, setFilterText] = useState('');
useEffect( useEffect(
() => { () => {

View File

@ -1,3 +1,5 @@
import Overlay from '../Common/Overlay';
interface SelectedCounterProps { interface SelectedCounterProps {
total: number total: number
selected: number selected: number
@ -13,11 +15,12 @@ function SelectedCounter({
return null; return null;
} }
return ( return (
<div className='relative w-full z-pop'> <Overlay
<div className={`absolute px-2 select-none whitespace-nowrap small-caps clr-app ${position}`}> position={`px-2 ${position}`}
className='select-none whitespace-nowrap small-caps clr-app'
>
Выбор {selected} из {total} Выбор {selected} из {total}
</div> </Overlay>);
</div>);
} }
export default SelectedCounter; export default SelectedCounter;

View File

@ -118,6 +118,5 @@ export const AuthState = ({ children }: AuthStateProps) => {
value={{ user, login, logout, signup, loading, error, setError, updatePassword }} value={{ user, login, logout, signup, loading, error, setError, updatePassword }}
> >
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>);
);
}; };

View File

@ -43,14 +43,14 @@ interface LibraryStateProps {
} }
export const LibraryState = ({ children }: LibraryStateProps) => { export const LibraryState = ({ children }: LibraryStateProps) => {
const [ items, setItems ] = useState<ILibraryItem[]>([]);
const [ templates, setTemplates ] = useState<ILibraryItem[]>([]);
const [ loading, setLoading ] = useState(false);
const [ processing, setProcessing ] = useState(false);
const [ error, setError ] = useState<ErrorInfo>(undefined);
const { user } = useAuth(); const { user } = useAuth();
const [ cachedTemplates, setCachedTemplates ] = useState<IRSForm[]>([]); const [items, setItems] = useState<ILibraryItem[]>([]);
const [templates, setTemplates] = useState<ILibraryItem[]>([]);
const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined);
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
const applyFilter = useCallback( const applyFilter = useCallback(
(params: ILibraryFilter) => { (params: ILibraryFilter) => {
@ -194,7 +194,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
applyFilter, createItem, cloneItem, destroyItem, retrieveTemplate, applyFilter, createItem, cloneItem, destroyItem, retrieveTemplate,
localUpdateItem, localUpdateTimestamp localUpdateItem, localUpdateTimestamp
}}> }}>
{ children } {children}
</LibraryContext.Provider> </LibraryContext.Provider>);
);
} }

View File

@ -55,6 +55,5 @@ export const NavigationState = ({ children }: NavigationStateProps) => {
navigateTo, navigateHistory navigateTo, navigateHistory
}}> }}>
{children} {children}
</NagivationContext.Provider> </NagivationContext.Provider>);
);
} }

View File

@ -1,23 +1,23 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react' import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { type ErrorInfo } from '../components/BackendError' import { type ErrorInfo } from '../components/BackendError';
import { useRSFormDetails } from '../hooks/useRSFormDetails' import { useRSFormDetails } from '../hooks/useRSFormDetails';
import { ILibraryItem } from '../models/library' import { ILibraryItem } from '../models/library';
import { ILibraryUpdateData } from '../models/library' import { ILibraryUpdateData } from '../models/library';
import { import {
IConstituentaList, IConstituentaMeta, ICstCreateData, IConstituentaList, IConstituentaMeta, ICstCreateData,
ICstMovetoData, ICstRenameData, ICstUpdateData, ICstMovetoData, ICstRenameData, ICstUpdateData,
IRSForm, IRSFormUploadData IRSForm, IRSFormUploadData
} from '../models/rsform' } from '../models/rsform';
import { import {
type DataCallback, deleteUnsubscribe, type DataCallback, deleteUnsubscribe,
getTRSFile, getTRSFile,
patchConstituenta, patchDeleteConstituenta, patchConstituenta, patchDeleteConstituenta,
patchLibraryItem, patchLibraryItem,
patchMoveConstituenta, patchRenameConstituenta, patchMoveConstituenta, patchRenameConstituenta,
patchResetAliases, patchUploadTRS, postClaimLibraryItem, postNewConstituenta, postSubscribe} from '../utils/backendAPI' patchResetAliases, patchUploadTRS, postClaimLibraryItem, postNewConstituenta, postSubscribe} from '../utils/backendAPI';
import { useAuth } from './AuthContext' import { useAuth } from './AuthContext';
import { useLibrary } from './LibraryContext' import { useLibrary } from './LibraryContext';
interface IRSFormContext { interface IRSFormContext {
schema?: IRSForm schema?: IRSForm
@ -27,14 +27,14 @@ interface IRSFormContext {
processing: boolean processing: boolean
isMutable: boolean isMutable: boolean
adminMode: boolean
isOwned: boolean isOwned: boolean
isClaimable: boolean isClaimable: boolean
isReadonly: boolean
isTracking: boolean isTracking: boolean
toggleForceAdmin: () => void adminMode: boolean
toggleReadonly: () => void toggleAdminMode: () => void
readerMode: boolean
toggleReaderMode: () => void
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void
claim: (callback?: DataCallback<ILibraryItem>) => void claim: (callback?: DataCallback<ILibraryItem>) => void
@ -56,11 +56,9 @@ const RSFormContext = createContext<IRSFormContext | null>(null)
export const useRSForm = () => { export const useRSForm = () => {
const context = useContext(RSFormContext) const context = useContext(RSFormContext)
if (context === null) { if (context === null) {
throw new Error( throw new Error('useRSForm has to be used within <RSFormState.Provider>');
'useRSForm has to be used within <RSFormState.Provider>'
)
} }
return context return context;
} }
interface RSFormStateProps { interface RSFormStateProps {
@ -72,11 +70,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const library = useLibrary(); const library = useLibrary();
const { user } = useAuth(); const { user } = useAuth();
const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID }); const { schema, reload, error, setError, setSchema, loading } = useRSFormDetails({ target: schemaID });
const [ processing, setProcessing ] = useState(false); const [processing, setProcessing] = useState(false);
const [ adminMode, setAdminMode ] = useState(false); const [adminMode, setAdminMode] = useState(false);
const [ isReadonly, setIsReadonly ] = useState(false); const [readerMode, setReaderMode] = useState(false);
const [ toggleTracking, setToggleTracking ] = useState(false); const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo( const isOwned = useMemo(
() => { () => {
@ -91,10 +89,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const isMutable = useMemo( const isMutable = useMemo(
() => { () => {
return ( return (
!loading && !processing && !isReadonly && !loading && !processing && !readerMode &&
((isOwned || (adminMode && user?.is_staff)) ?? false) ((isOwned || (adminMode && user?.is_staff)) ?? false)
); );
}, [user?.is_staff, isReadonly, adminMode, isOwned, loading, processing]); }, [user?.is_staff, readerMode, adminMode, isOwned, loading, processing]);
const isTracking = useMemo( const isTracking = useMemo(
() => { () => {
@ -322,14 +320,13 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{
schema, schema,
error, loading, processing, error, loading, processing,
adminMode, isReadonly, isOwned, isMutable, adminMode, readerMode, isOwned, isMutable,
isClaimable, isTracking, isClaimable, isTracking,
toggleForceAdmin: () => setAdminMode(prev => !prev),
toggleReadonly: () => setIsReadonly(prev => !prev),
update, download, upload, claim, resetAliases, subscribe, unsubscribe, update, download, upload, claim, resetAliases, subscribe, unsubscribe,
cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo cstUpdate, cstCreate, cstRename, cstDelete, cstMoveTo,
toggleAdminMode: () => setAdminMode(prev => !prev),
toggleReaderMode: () => setReaderMode(prev => !prev)
}}> }}>
{ children } { children }
</RSFormContext.Provider> </RSFormContext.Provider>);
);
} }

View File

@ -84,6 +84,5 @@ export const ThemeState = ({ children }: ThemeStateProps) => {
viewportHeight, mainHeight viewportHeight, mainHeight
}}> }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>);
);
} }

View File

@ -68,7 +68,7 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
if (callback) callback(newData); if (callback) callback(newData);
} }
}); });
}, [setUser, users]); }, [setUser, users, user?.id]);
useEffect(() => { useEffect(() => {
reload(); reload();

View File

@ -67,6 +67,5 @@ export const UsersState = ({ children }: UsersStateProps) => {
getUserLabel getUserLabel
}}> }}>
{ children } { children }
</UsersContext.Provider> </UsersContext.Provider>);
);
} }

View File

@ -2,11 +2,11 @@ import { useLayoutEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import ConceptTab from '../../components/Common/ConceptTab'; import ConceptTab from '../../components/Common/ConceptTab';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Modal, { ModalProps } from '../../components/Common/Modal'; import Modal, { ModalProps } from '../../components/Common/Modal';
import HelpRSTemplates from '../../components/Help/HelpRSTemplates'; import Overlay from '../../components/Common/Overlay';
import { HelpIcon } from '../../components/Icons'; import HelpButton from '../../components/Help/HelpButton';
import usePartialUpdate from '../../hooks/usePartialUpdate'; import usePartialUpdate from '../../hooks/usePartialUpdate';
import { HelpTopic } from '../../models/miscelanious';
import { CstType, ICstCreateData, IRSForm } from '../../models/rsform'; import { CstType, ICstCreateData, IRSForm } from '../../models/rsform';
import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslangAPI'; import { inferTemplatedType, substituteTemplateArgs } from '../../models/rslangAPI';
import { createAliasFor, validateCstAlias } from '../../utils/misc'; import { createAliasFor, validateCstAlias } from '../../utils/misc';
@ -115,15 +115,17 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'
> >
<div className='max-w-[40rem] min-w-[40rem] min-h-[35rem] px-2 mb-1'>
<Tabs defaultFocus forceRenderTabPanel <Tabs defaultFocus forceRenderTabPanel
className='flex flex-col items-center' className='flex flex-col items-center max-w-[40rem] min-w-[40rem] min-h-[35rem] mb-1'
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<div className='flex gap-1 pl-6 mb-3'> <Overlay position='top-0 left-[12.3rem]'>
<TabList className='flex border'> <HelpButton topic={HelpTopic.RSTEMPLATES} dimensions='max-w-[35rem]' />
</Overlay>
<TabList className='flex mb-3 border'>
<ConceptTab <ConceptTab
label='Шаблон' label='Шаблон'
tooltip='Выбор шаблона выражения' tooltip='Выбор шаблона выражения'
@ -141,18 +143,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
/> />
</TabList> </TabList>
<div id='templates-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip
anchorSelect='#templates-help'
className='max-w-[35rem] z-modal-tooltip'
offset={12}
>
<HelpRSTemplates />
</ConceptTooltip>
</div>
<div className='w-full'> <div className='w-full'>
<TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '': 'none' }}> <TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '': 'none' }}>
<TemplateTab <TemplateTab
@ -177,7 +167,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
</TabPanel> </TabPanel>
</div> </div>
</Tabs> </Tabs>
</div>
</Modal>); </Modal>);
} }

View File

@ -21,7 +21,7 @@ interface TemplateTabProps {
function TemplateTab({ state, partialUpdate }: TemplateTabProps) { function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
const { templates, retrieveTemplate } = useLibrary(); const { templates, retrieveTemplate } = useLibrary();
const [ selectedSchema, setSelectedSchema ] = useState<IRSForm | undefined>(undefined); const [selectedSchema, setSelectedSchema] = useState<IRSForm | undefined>(undefined);
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]); const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);

View File

@ -15,7 +15,7 @@ extends Pick<ModalProps, 'hideWindow'> {
function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) { function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
const [ expandOut, setExpandOut ] = useState(false); const [expandOut, setExpandOut] = useState(false);
const expansion: number[] = useMemo(() => schema?.graph.expandOutputs(selected) ?? [], [selected, schema?.graph]); const expansion: number[] = useMemo(() => schema?.graph.expandOutputs(selected) ?? [], [selected, schema?.graph]);
function handleSubmit() { function handleSubmit() {
@ -28,11 +28,10 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
} }
return ( return (
<Modal <Modal canSubmit
title='Удаление конституент' title='Удаление конституент'
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'} submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={true}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='max-w-[60vw] min-w-[30rem]'> <div className='max-w-[60vw] min-w-[30rem]'>

View File

@ -2,11 +2,11 @@ import { useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import ConceptTab from '../../components/Common/ConceptTab'; import ConceptTab from '../../components/Common/ConceptTab';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import HelpTerminologyControl from '../../components/Help/HelpTerminologyControl'; import Overlay from '../../components/Common/Overlay';
import { HelpIcon } from '../../components/Icons'; import HelpButton from '../../components/Help/HelpButton';
import { ReferenceType } from '../../models/language'; import { ReferenceType } from '../../models/language';
import { HelpTopic } from '../../models/miscelanious';
import { IConstituenta } from '../../models/rsform'; import { IConstituenta } from '../../models/rsform';
import { labelReferenceType } from '../../utils/labels'; import { labelReferenceType } from '../../utils/labels';
import EntityTab from './EntityTab'; import EntityTab from './EntityTab';
@ -48,15 +48,17 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='min-w-[40rem] max-w-[40rem] flex flex-col gap-3 mb-2 min-h-[34rem]'>
<Tabs defaultFocus <Tabs defaultFocus
className='flex flex-col items-center' className='flex flex-col items-center min-w-[40rem] max-w-[40rem] mb-2 min-h-[34rem]'
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
selectedIndex={activeTab} selectedIndex={activeTab}
onSelect={setActiveTab} onSelect={setActiveTab}
> >
<div className='flex gap-1 pl-6 mb-3'> <Overlay position='top-0 left-[12.2rem]'>
<TabList className='flex border'> <HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={14} />
</Overlay>
<TabList className='flex mb-3 border'>
<ConceptTab <ConceptTab
label={labelReferenceType(ReferenceType.ENTITY)} label={labelReferenceType(ReferenceType.ENTITY)}
tooltip='Отсылка на термин в заданной словоформе' tooltip='Отсылка на термин в заданной словоформе'
@ -69,18 +71,6 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
/> />
</TabList> </TabList>
<div id='terminology-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip
anchorSelect='#terminology-help'
className='max-w-[30rem] z-modal-tooltip'
offset={10}
>
<HelpTerminologyControl />
</ConceptTooltip>
</div>
<div className='w-full'> <div className='w-full'>
<TabPanel> <TabPanel>
<EntityTab <EntityTab
@ -100,7 +90,6 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
</TabPanel> </TabPanel>
</div> </div>
</Tabs> </Tabs>
</div>
</Modal>); </Modal>);
} }

View File

@ -1,17 +1,18 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import ConceptTooltip from '../components/Common/ConceptTooltip';
import MiniButton from '../components/Common/MiniButton'; import MiniButton from '../components/Common/MiniButton';
import Modal from '../components/Common/Modal'; import Modal from '../components/Common/Modal';
import Overlay from '../components/Common/Overlay';
import SelectMulti from '../components/Common/SelectMulti'; import SelectMulti from '../components/Common/SelectMulti';
import TextArea from '../components/Common/TextArea'; import TextArea from '../components/Common/TextArea';
import DataTable, { createColumnHelper } from '../components/DataTable'; import DataTable, { createColumnHelper } from '../components/DataTable';
import HelpTerminologyControl from '../components/Help/HelpTerminologyControl'; import HelpButton from '../components/Help/HelpButton';
import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossIcon, HelpIcon } from '../components/Icons'; import { ArrowLeftIcon, ArrowRightIcon, CheckIcon, ChevronDoubleDownIcon, CrossIcon } from '../components/Icons';
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, parseGrammemes,wordFormEquals } from '../models/languageAPI'; import { getCompatibleGrams, parseGrammemes,wordFormEquals } from '../models/languageAPI';
import { HelpTopic } from '../models/miscelanious';
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';
@ -204,37 +205,22 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
], [colors]); ], [colors]);
return ( return (
<Modal <Modal canSubmit
title='Редактирование словоформ' title='Редактирование словоформ'
hideWindow={hideWindow} hideWindow={hideWindow}
submitText='Сохранить' submitText='Сохранить'
canSubmit
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='relative w-full'>
<div className='absolute top-0 right-0'>
<div id='terminology-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip
anchorSelect='#terminology-help'
className='max-w-[40rem]'
layer='z-modal-tooltip'
offset={1}
>
<HelpTerminologyControl />
</ConceptTooltip>
</div>
</div>
<div className='min-w-[40rem] max-w-[40rem]'> <div className='min-w-[40rem] max-w-[40rem]'>
<TextArea id='nominal' label='Начальная форма' <Overlay position='top-[-0.2rem] left-[7.5rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
</Overlay>
<TextArea disabled spellCheck
label='Начальная форма'
placeholder='Начальная форма' placeholder='Начальная форма'
rows={1} rows={1}
value={term} value={term}
disabled={true}
spellCheck
/> />
<div className='mt-3 mb-2 text-sm font-semibold'> <div className='mt-3 mb-2 text-sm font-semibold'>
@ -245,8 +231,8 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<div className='flex items-center'> <div className='flex items-center'>
<TextArea <TextArea
placeholder='Введите текст' placeholder='Введите текст'
rows={2}
dimensions='min-w-[18rem] w-full min-h-[4.2rem]' dimensions='min-w-[18rem] w-full min-h-[4.2rem]'
rows={2}
value={inputText} value={inputText}
onChange={event => setInputText(event.target.value)} onChange={event => setInputText(event.target.value)}
/> />
@ -289,7 +275,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton
tooltip='Генерировать все словоформы' tooltip='Генерировать стандартные словоформы'
icon={<ChevronDoubleDownIcon icon={<ChevronDoubleDownIcon
size={5} size={5}
color={!inputText ? 'text-disabled' : 'text-primary'} color={!inputText ? 'text-disabled' : 'text-primary'}
@ -302,7 +288,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
Заданные вручную словоформы [{forms.length}] Заданные вручную словоформы [{forms.length}]
</div> </div>
<MiniButton <MiniButton
tooltip='Сбросить ВСЕ словоформы' tooltip='Сбросить все словоформы'
icon={<CrossIcon size={5} color={forms.length === 0 ? 'text-disabled' : 'text-warning'} />} icon={<CrossIcon size={5} color={forms.length === 0 ? 'text-disabled' : 'text-warning'} />}
disabled={textProcessor.loading || forms.length === 0} disabled={textProcessor.loading || forms.length === 0}
onClick={handleResetAll} onClick={handleResetAll}

View File

@ -48,8 +48,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='flex justify-center w-full min-w-[22rem] my-3'> <div className='flex justify-center items-center gap-6 w-full min-w-[22rem] my-3'>
<div className='flex items-center gap-6'>
<SelectSingle <SelectSingle
placeholder='Выберите тип' placeholder='Выберите тип'
className='min-w-[14rem] self-center' className='min-w-[14rem] self-center'
@ -61,14 +60,14 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
onChange={data => updateData({cst_type: data?.value ?? CstType.BASE})} onChange={data => updateData({cst_type: data?.value ?? CstType.BASE})}
/> />
<div> <div>
<TextInput id='alias' label='Имя' dense <TextInput dense
label='Имя'
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})}
/> />
</div> </div>
</div> </div>
</div>
</Modal>); </Modal>);
} }

View File

@ -57,8 +57,7 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
hideWindow={hideWindow} hideWindow={hideWindow}
readonly readonly
> >
<div className='flex flex-col items-start gap-2 mt-2'> <div className='w-full my-2 text-lg text-center'>
<div className='w-full text-lg text-center'>
{!hoverNode ? expression : null} {!hoverNode ? expression : null}
{hoverNode ? {hoverNode ?
<div> <div>
@ -86,7 +85,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
/> />
</div> </div>
</div> </div>
</div>
</Modal>); </Modal>);
} }

View File

@ -1,8 +1,8 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react';
import { type ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError';
import { CstType, IConstituenta, type IRSForm } from '../models/rsform'; import { CstType, IConstituenta, type IRSForm } from '../models/rsform';
import { IExpressionParse, IArgumentInfo } from '../models/rslang'; import { IArgumentInfo,IExpressionParse } from '../models/rslang';
import { RSErrorType } from '../models/rslang'; import { RSErrorType } from '../models/rslang';
import { DataCallback, postCheckExpression } from '../utils/backendAPI'; import { DataCallback, postCheckExpression } from '../utils/backendAPI';
import { getCstExpressionPrefix } from '../utils/misc'; import { getCstExpressionPrefix } from '../utils/misc';

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '../models/language'; import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '../models/language';

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react';
import { type ErrorInfo } from '../components/BackendError'; import { type ErrorInfo } from '../components/BackendError';
import { IRSForm, IRSFormData } from '../models/rsform' import { IRSForm, IRSFormData } from '../models/rsform';
import { loadRSFormData } from '../models/rsformAPI'; import { loadRSFormData } from '../models/rsformAPI';
import { getRSFormDetails } from '../utils/backendAPI'; import { getRSFormDetails } from '../utils/backendAPI';

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { IResolutionData } from '../models/language'; import { IResolutionData } from '../models/language';

View File

@ -100,42 +100,42 @@ export const Case = [
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Plurality = [ Grammeme.sing, Grammeme.plur ]; export const Plurality = [Grammeme.sing, Grammeme.plur];
/** /**
* Represents verb perfectivity language concept. * Represents verb perfectivity language concept.
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Perfectivity = [ Grammeme.perf, Grammeme.impf ]; export const Perfectivity = [Grammeme.perf, Grammeme.impf];
/** /**
* Represents verb transitivity language concept. * Represents verb transitivity language concept.
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Transitivity = [ Grammeme.tran, Grammeme.intr ]; export const Transitivity = [Grammeme.tran, Grammeme.intr];
/** /**
* Represents verb mood language concept. * Represents verb mood language concept.
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Mood = [ Grammeme.indc, Grammeme.impr ]; export const Mood = [Grammeme.indc, Grammeme.impr];
/** /**
* Represents verb self-inclusion language concept. * Represents verb self-inclusion language concept.
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Inclusion = [ Grammeme.incl, Grammeme.excl ]; export const Inclusion = [Grammeme.incl, Grammeme.excl];
/** /**
* Represents verb voice language concept. * Represents verb voice language concept.
* *
* Implemented as a list of mututally exclusive {@link Grammeme}s. * Implemented as a list of mututally exclusive {@link Grammeme}s.
*/ */
export const Voice = [ Grammeme.actv, Grammeme.pssv ]; export const Voice = [Grammeme.actv, Grammeme.pssv];
/** /**
* Represents verb tense language concept. * Represents verb tense language concept.

View File

@ -8,6 +8,7 @@ import Checkbox from '../components/Common/Checkbox';
import Form from '../components/Common/Form'; import Form from '../components/Common/Form';
import Label from '../components/Common/Label'; import Label from '../components/Common/Label';
import MiniButton from '../components/Common/MiniButton'; import MiniButton from '../components/Common/MiniButton';
import Overlay from '../components/Common/Overlay';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextArea from '../components/Common/TextArea'; import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
@ -78,13 +79,12 @@ function CreateRSFormPage() {
return ( return (
<RequireAuth> <RequireAuth>
<div className='flex justify-center w-full'> <div className='flex justify-center'>
<Form title='Создание концептуальной схемы' <Form title='Создание концептуальной схемы'
onSubmit={handleSubmit} onSubmit={handleSubmit}
dimensions='max-w-lg w-full mt-4' dimensions='max-w-lg w-full mt-4'
> >
<div className='relative w-full'> <Overlay position='top-[-2.4rem] right-[-1rem]'>
<div className='absolute top-[-2.4rem] right-[-1rem] flex'>
<input ref={inputRef} type='file' <input ref={inputRef} type='file'
style={{ display: 'none' }} style={{ display: 'none' }}
accept={EXTEOR_TRS_FILE} accept={EXTEOR_TRS_FILE}
@ -95,10 +95,9 @@ function CreateRSFormPage() {
icon={<DownloadIcon size={5} color='text-primary'/>} icon={<DownloadIcon size={5} color='text-primary'/>}
onClick={() => inputRef.current?.click()} onClick={() => inputRef.current?.click()}
/> />
</div> </Overlay>
</div>
<div className='flex flex-col gap-3'>
{fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null} {fileName ? <Label text={`Загружен файл: ${fileName}`} /> : null}
<TextInput <TextInput
label='Полное название' label='Полное название'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
@ -137,7 +136,6 @@ function CreateRSFormPage() {
/> />
</div> </div>
{error ? <BackendError error={error} /> : null} {error ? <BackendError error={error} /> : null}
</div>
</Form> </Form>
</div> </div>
</RequireAuth>); </RequireAuth>);

View File

@ -0,0 +1,31 @@
import { EducationIcon, GroupIcon, SubscribedIcon } from '../../components/Icons';
import { ICurrentUser, ILibraryItem } from '../../models/library';
import { prefixes } from '../../utils/constants';
interface ItemIconsProps {
user?: ICurrentUser
item: ILibraryItem
}
function ItemIcons({ user, item }: ItemIconsProps) {
return (
<div
className='inline-flex items-center justify-start gap-1 min-w-[2.75rem]'
id={`${prefixes.library_list}${item.id}`}
>
{(user && user.subscriptions.includes(item.id)) ?
<span title='Отслеживаемая'>
<SubscribedIcon size={3} />
</span> : null}
{item.is_common ?
<span title='Общедоступная'>
<GroupIcon size={3}/>
</span> : null}
{item.is_canonical ?
<span title='Неизменная'>
<EducationIcon size={3}/>
</span> : null}
</div>);
}
export default ItemIcons;

View File

@ -1,7 +1,7 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import BackendError from '../../components/BackendError' import BackendError from '../../components/BackendError';
import { ConceptLoader } from '../../components/Common/ConceptLoader' import { ConceptLoader } from '../../components/Common/ConceptLoader';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
@ -14,8 +14,8 @@ function LibraryPage() {
const library = useLibrary(); const library = useLibrary();
const { setShowScroll } = useConceptTheme(); const { setShowScroll } = useConceptTheme();
const [ filter, setFilter ] = useState<ILibraryFilter>({}); const [filter, setFilter] = useState<ILibraryFilter>({});
const [ items, setItems ] = useState<ILibraryItem[]>([]); const [items, setItems] = useState<ILibraryItem[]>([]);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [strategy, setStrategy] = useLocalStorage<LibraryFilterStrategy>('search_strategy', LibraryFilterStrategy.MANUAL); const [strategy, setStrategy] = useLocalStorage<LibraryFilterStrategy>('search_strategy', LibraryFilterStrategy.MANUAL);
@ -42,7 +42,7 @@ function LibraryPage() {
{library.loading ? <ConceptLoader/> : null} {library.loading ? <ConceptLoader/> : null}
{library.error ? <BackendError error={library.error}/> : null} {library.error ? <BackendError error={library.error}/> : null}
{(!library.loading && library.items) ? {(!library.loading && library.items) ?
<div className='flex flex-col w-full'> <>
<SearchPanel <SearchPanel
query={query} query={query}
setQuery={setQuery} setQuery={setQuery}
@ -56,7 +56,7 @@ function LibraryPage() {
resetQuery={resetQuery} resetQuery={resetQuery}
items={items} items={items}
/> />
</div> : null} </> : null}
</>); </>);
} }

View File

@ -1,17 +1,16 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import TextURL from '../../components/Common/TextURL'; import TextURL from '../../components/Common/TextURL';
import DataTable, { createColumnHelper } from '../../components/DataTable'; import DataTable, { createColumnHelper } from '../../components/DataTable';
import HelpLibrary from '../../components/Help/HelpLibrary'; import HelpButton from '../../components/Help/HelpButton';
import { EducationIcon, GroupIcon, HelpIcon,SubscribedIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useConceptNavigation } from '../../context/NagivationContext'; import { useConceptNavigation } from '../../context/NagivationContext';
import { useUsers } from '../../context/UsersContext'; import { useUsers } from '../../context/UsersContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { ILibraryItem } from '../../models/library'; import { ILibraryItem } from '../../models/library';
import { prefixes } from '../../utils/constants'; import { HelpTopic } from '../../models/miscelanious';
import ItemIcons from './ItemIcons';
interface ViewLibraryProps { interface ViewLibraryProps {
items: ILibraryItem[] items: ILibraryItem[]
@ -25,10 +24,9 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
const intl = useIntl(); const intl = useIntl();
const { user } = useAuth(); const { user } = useAuth();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const [itemsPerPage, setItemsPerPage] = useLocalStorage<number>('library_per_page', 50);
const [ itemsPerPage, setItemsPerPage ] = useLocalStorage<number>('library_per_page', 50); const handleOpenItem = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
const openRSForm = (item: ILibraryItem) => navigateTo(`/rsforms/${item.id}`);
const columns = useMemo( const columns = useMemo(
() => [ () => [
@ -38,28 +36,11 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
size: 60, size: 60,
minSize: 60, minSize: 60,
maxSize: 60, maxSize: 60,
cell: props => { cell: props =>
const item = props.row.original; <ItemIcons
return (<> item={props.row.original}
<div user={user}
className='flex items-center justify-start gap-1 min-w-[2.75rem]' />,
id={`${prefixes.library_list}${item.id}`}
>
{(user && user.subscriptions.includes(item.id)) ?
<p title='Отслеживаемая'>
<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>
</>);
},
}), }),
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
id: 'alias', id: 'alias',
@ -95,7 +76,10 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
size: 150, size: 150,
minSize: 150, minSize: 150,
maxSize: 150, maxSize: 150,
cell: props => <div className='text-sm min-w-[8.25rem]'>{new Date(props.cell.getValue()).toLocaleString(intl.locale)}</div>, cell: props =>
<div className='text-sm min-w-[8.25rem]'>
{new Date(props.cell.getValue()).toLocaleString(intl.locale)}
</div>,
enableSorting: true, enableSorting: true,
sortingFn: 'datetime', sortingFn: 'datetime',
sortDescFirst: true sortDescFirst: true
@ -104,36 +88,30 @@ function ViewLibrary({ items, resetQuery: cleanQuery }: ViewLibraryProps) {
return ( return (
<> <>
{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.25rem] flex gap-1 ml-3 z-pop'>
<div id='library-help' className='py-2 '> <HelpButton
<HelpIcon color='text-primary' size={5} /> topic={HelpTopic.LIBRARY}
dimensions='max-w-[35rem]'
offset={0}
/>
</div> </div>
<ConceptTooltip anchorSelect='#library-help'>
<div className='max-w-[35rem]'>
<HelpLibrary />
</div> </div>
</ConceptTooltip>
</div>
</div> : null}
<DataTable <DataTable
columns={columns} columns={columns}
data={items} data={items}
headPosition='2.3rem' headPosition='2.3rem'
noDataComponent={ noDataComponent={
<div className='flex flex-col gap-4 justify-center p-2 text-center min-h-[6rem]'> <div className='p-3 text-center min-h-[6rem]'>
<p>Список схем пуст</p> <p>Список схем пуст</p>
<p className='flex justify-center gap-4'> <p className='flex justify-center gap-6 mt-3'>
<TextURL text='Создать схему' href='/rsform-create'/> <TextURL text='Создать схему' href='/rsform-create'/>
<span className='cursor-pointer hover:underline text-url' onClick={cleanQuery}> <TextURL text='Очистить фильтр' onClick={cleanQuery} />
Очистить фильтр
</span>
</p> </p>
</div>} </div>}
onRowClicked={openRSForm} onRowClicked={handleOpenItem}
enableSorting enableSorting
initialSorting={{ initialSorting={{

View File

@ -23,8 +23,7 @@ function TopicsList({ activeTopic, onChangeTopic }: TopicsListProps) {
{labelHelpTopic(topic)} {labelHelpTopic(topic)}
</div>); </div>);
})} })}
</div> </div>);
);
} }
export default TopicsList; export default TopicsList;

View File

@ -1,14 +1,4 @@
import HelpAPI from '../../components/Help/HelpAPI'; import InfoTopic from '../../components/Help/InfoTopic';
import HelpConstituenta from '../../components/Help/HelpConstituenta';
import HelpExteor from '../../components/Help/HelpExteor';
import HelpLibrary from '../../components/Help/HelpLibrary';
import HelpMain from '../../components/Help/HelpMain';
import HelpRSFormItems from '../../components/Help/HelpRSFormItems';
import HelpRSFormMeta from '../../components/Help/HelpRSFormMeta';
import HelpRSLang from '../../components/Help/HelpRSLang';
import HelpRSTemplates from '../../components/Help/HelpRSTemplates';
import HelpTermGraph from '../../components/Help/HelpTermGraph';
import HelpTerminologyControl from '../../components/Help/HelpTerminologyControl';
import { HelpTopic } from '../../models/miscelanious'; import { HelpTopic } from '../../models/miscelanious';
interface ViewTopicProps { interface ViewTopicProps {
@ -18,17 +8,7 @@ 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 /> : null} <InfoTopic topic={topic}/>
{topic === HelpTopic.LIBRARY ? <HelpLibrary /> : null}
{topic === HelpTopic.RSFORM ? <HelpRSFormMeta /> : null}
{topic === HelpTopic.CSTLIST ? <HelpRSFormItems /> : null}
{topic === HelpTopic.CONSTITUENTA ? <HelpConstituenta /> : null}
{topic === HelpTopic.GRAPH_TERM ? <HelpTermGraph /> : null}
{topic === HelpTopic.RSTEMPLATES ? <HelpRSTemplates /> : null}
{topic === HelpTopic.RSLANG ? <HelpRSLang /> : null}
{topic === HelpTopic.TERM_CONTROL ? <HelpTerminologyControl /> : null}
{topic === HelpTopic.EXTEOR ? <HelpExteor /> : null}
{topic === HelpTopic.API ? <HelpAPI /> : null}
</div>); </div>);
} }

View File

@ -1,9 +1,12 @@
import { useMemo } from 'react' import { useMemo } from 'react';
import ConceptTooltip from '../../../components/Common/ConceptTooltip' import MiniButton from '../../../components/Common/MiniButton';
import MiniButton from '../../../components/Common/MiniButton' import Overlay from '../../../components/Common/Overlay';
import HelpConstituenta from '../../../components/Help/HelpConstituenta' import HelpButton from '../../../components/Help/HelpButton';
import { ArrowsRotateIcon, CloneIcon, DiamondIcon, DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../../components/Icons' import {
ArrowsRotateIcon, CloneIcon, DiamondIcon, DumpBinIcon, SaveIcon, SmallPlusIcon
} from '../../../components/Icons';
import { HelpTopic } from '../../../models/miscelanious';
interface ConstituentaToolbarProps { interface ConstituentaToolbarProps {
isMutable: boolean isMutable: boolean
@ -25,9 +28,7 @@ function ConstituentaToolbar({
}: ConstituentaToolbarProps) { }: ConstituentaToolbarProps) {
const canSave = useMemo(() => (isModified && isMutable), [isModified, isMutable]); const canSave = useMemo(() => (isModified && isMutable), [isModified, isMutable]);
return ( return (
<div className='relative w-full'> <Overlay position='right-1/2 translate-x-1/2 top-1 flex items-start'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<div className='flex justify-start select-auto w-fit z-tooltip'>
<MiniButton <MiniButton
tooltip='Сохранить изменения' tooltip='Сохранить изменения'
disabled={!canSave} disabled={!canSave}
@ -64,18 +65,8 @@ function ConstituentaToolbar({
onClick={onDelete} onClick={onDelete}
icon={<DumpBinIcon size={5} color={isMutable ? 'text-warning' : ''} />} icon={<DumpBinIcon size={5} color={isMutable ? 'text-warning' : ''} />}
/> />
<div id='cst-help' className='px-1 py-1'> <HelpButton topic={HelpTopic.CONSTITUENTA} offset={4} />
<HelpIcon color='text-primary' size={5} /> </Overlay>);
</div>
<ConceptTooltip
anchorSelect='#cst-help'
offset={4}
>
<HelpConstituenta />
</ConceptTooltip>
</div>
</div>
</div>);
} }
export default ConstituentaToolbar; export default ConstituentaToolbar;

View File

@ -5,6 +5,7 @@ import { toast } from 'react-toastify';
import Button from '../../../components/Common/Button'; import Button from '../../../components/Common/Button';
import { ConceptLoader } from '../../../components/Common/ConceptLoader'; import { ConceptLoader } from '../../../components/Common/ConceptLoader';
import MiniButton from '../../../components/Common/MiniButton'; import MiniButton from '../../../components/Common/MiniButton';
import Overlay from '../../../components/Common/Overlay';
import { ASTNetworkIcon } from '../../../components/Icons'; import { ASTNetworkIcon } from '../../../components/Icons';
import RSInput from '../../../components/RSInput'; import RSInput from '../../../components/RSInput';
import { RSTextWrapper } from '../../../components/RSInput/textEditing'; import { RSTextWrapper } from '../../../components/RSInput/textEditing';
@ -118,16 +119,13 @@ function EditorRSExpression({
return ( return (
<div className='flex flex-col items-start w-full'> <div className='flex flex-col items-start w-full'>
<div className='relative w-full'> <Overlay position='top-[-0.2rem] left-[11rem]'>
<div className='absolute top-[-0.2rem] left-[11rem]'> <MiniButton noHover
<MiniButton
tooltip='Дерево разбора выражения' tooltip='Дерево разбора выражения'
noHover
onClick={handleShowAST} onClick={handleShowAST}
icon={<ASTNetworkIcon size={5} color='text-primary' />} icon={<ASTNetworkIcon size={5} color='text-primary' />}
/> />
</div> </Overlay>
</div>
<RSInput innerref={rsInput} <RSInput innerref={rsInput}
value={value} value={value}
minHeight='3.8rem' minHeight='3.8rem'

View File

@ -2,6 +2,7 @@ import { Dispatch, SetStateAction, useLayoutEffect, useMemo, useState } from 're
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import MiniButton from '../../../components/Common/MiniButton'; import MiniButton from '../../../components/Common/MiniButton';
import Overlay from '../../../components/Common/Overlay';
import SubmitButton from '../../../components/Common/SubmitButton'; import SubmitButton from '../../../components/Common/SubmitButton';
import TextArea from '../../../components/Common/TextArea'; import TextArea from '../../../components/Common/TextArea';
import { EditIcon, SaveIcon } from '../../../components/Icons'; import { EditIcon, SaveIcon } from '../../../components/Icons';
@ -101,11 +102,9 @@ function FormConstituenta({
} }
return (<> return (<>
{readyForEdit ? <Overlay position='top-0 left-[3rem]' className='flex justify-start select-none' >
<div className='relative'>
<div className='absolute top-0 right-[-3rem] w-full flex justify-start select-none'>
<MiniButton <MiniButton
tooltip={`Редактировать словоформы термина: ${constituenta!.term_forms.length}`} tooltip={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`}
disabled={!readyForEdit} disabled={!readyForEdit}
noHover noHover
onClick={onEditTerm} onClick={onEditTerm}
@ -121,8 +120,7 @@ function FormConstituenta({
onClick={handleRename} onClick={handleRename}
icon={<EditIcon size={4} color={readyForEdit ? 'text-primary' : ''} />} icon={<EditIcon size={4} color={readyForEdit ? 'text-primary' : ''} />}
/> />
</div> </Overlay>
</div> : null}
<form id={id} <form id={id}
className='flex flex-col gap-3 mt-1' className='flex flex-col gap-3 mt-1'
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@ -194,7 +194,7 @@ function ViewSideConstituents({ expression, baseHeight, activeID, onOpenEdit }:
}, [noNavigation, baseHeight]); }, [noNavigation, baseHeight]);
return (<> return (<>
<div className='sticky top-0 left-0 right-0 flex items-stretch justify-between gap-1 pl-2 border-b clr-input'> <div className='relative top-0 left-0 right-0 flex items-stretch justify-between gap-1 pl-2 border-b clr-input'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'> <div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon /> <MagnifyingGlassIcon />
</div> </div>

View File

@ -1,9 +1,10 @@
import { useMemo } from 'react' import { useMemo } from 'react';
import ConceptTooltip from '../../../components/Common/ConceptTooltip' import MiniButton from '../../../components/Common/MiniButton';
import MiniButton from '../../../components/Common/MiniButton' import Overlay from '../../../components/Common/Overlay';
import HelpRSFormMeta from '../../../components/Help/HelpRSFormMeta' import HelpButton from '../../../components/Help/HelpButton';
import { DownloadIcon, DumpBinIcon, HelpIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../../components/Icons' import { DownloadIcon, DumpBinIcon, OwnerIcon, SaveIcon, ShareIcon } from '../../../components/Icons';
import { HelpTopic } from '../../../models/miscelanious';
interface RSFormToolbarProps { interface RSFormToolbarProps {
isMutable: boolean isMutable: boolean
@ -25,8 +26,7 @@ function RSFormToolbar({
}: RSFormToolbarProps) { }: RSFormToolbarProps) {
const canSave = useMemo(() => (modified && isMutable), [modified, isMutable]); const canSave = useMemo(() => (modified && isMutable), [modified, isMutable]);
return ( return (
<div className='relative flex items-start justify-center w-full'> <Overlay position='w-full top-1 flex items-start justify-center'>
<div className='absolute flex mt-1'>
<MiniButton <MiniButton
tooltip='Сохранить изменения' tooltip='Сохранить изменения'
disabled={!canSave} disabled={!canSave}
@ -55,14 +55,8 @@ function RSFormToolbar({
onClick={onDestroy} onClick={onDestroy}
icon={<DumpBinIcon size={5} color={isMutable ? 'text-warning' : ''} />} icon={<DumpBinIcon size={5} color={isMutable ? 'text-warning' : ''} />}
/> />
<div id='rsform-help' className='py-1 ml-1'> <HelpButton topic={HelpTopic.RSFORM} offset={4} />
<HelpIcon color='text-primary' size={5} /> </Overlay>);
</div>
<ConceptTooltip anchorSelect='#rsform-help' offset={4}>
<HelpRSFormMeta />
</ConceptTooltip>
</div>
</div>);
} }
export default RSFormToolbar; export default RSFormToolbar;

View File

@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
import { type RowSelectionState } from '../../../components/DataTable'; import { type RowSelectionState } from '../../../components/DataTable';
import SelectedCounter from '../../../components/Shared/SelectedCounter'; import SelectedCounter from '../../../components/Shared/SelectedCounter';
import { useRSForm } from '../../../context/RSFormContext'; import { useRSForm } from '../../../context/RSFormContext';
import { CstType, ICstCreateData, ICstMovetoData } from '../../../models/rsform' import { CstType, ICstCreateData, ICstMovetoData } from '../../../models/rsform';
import RSListToolbar from './RSListToolbar'; import RSListToolbar from './RSListToolbar';
import RSTable from './RSTable'; import RSTable from './RSTable';

View File

@ -1,12 +1,13 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import ConceptTooltip from '../../../components/Common/ConceptTooltip';
import Dropdown from '../../../components/Common/Dropdown'; import Dropdown from '../../../components/Common/Dropdown';
import DropdownButton from '../../../components/Common/DropdownButton'; import DropdownButton from '../../../components/Common/DropdownButton';
import MiniButton from '../../../components/Common/MiniButton'; import MiniButton from '../../../components/Common/MiniButton';
import HelpRSFormItems from '../../../components/Help/HelpRSFormItems'; import Overlay from '../../../components/Common/Overlay';
import { ArrowDownIcon, ArrowDropdownIcon, ArrowUpIcon, CloneIcon, DiamondIcon, DumpBinIcon, HelpIcon, SmallPlusIcon, UpdateIcon } from '../../../components/Icons'; import HelpButton from '../../../components/Help/HelpButton';
import { ArrowDownIcon, ArrowDropdownIcon, ArrowUpIcon, CloneIcon, DiamondIcon, DumpBinIcon, SmallPlusIcon, UpdateIcon } from '../../../components/Icons';
import useDropdown from '../../../hooks/useDropdown'; import useDropdown from '../../../hooks/useDropdown';
import { HelpTopic } from '../../../models/miscelanious';
import { CstType } from '../../../models/rsform'; import { CstType } from '../../../models/rsform';
import { prefixes } from '../../../utils/constants'; import { prefixes } from '../../../utils/constants';
import { labelCstType } from '../../../utils/labels'; import { labelCstType } from '../../../utils/labels';
@ -34,8 +35,7 @@ function RSListToolbar({
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]); const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
return ( return (
<div className='relative w-full z-pop'> <Overlay position='w-full top-1 flex items-start justify-center'>
<div className='absolute flex items-start justify-center w-full top-1'>
<MiniButton <MiniButton
tooltip='Переместить вверх [Alt + вверх]' tooltip='Переместить вверх [Alt + вверх]'
icon={<ArrowUpIcon size={5}/>} icon={<ArrowUpIcon size={5}/>}
@ -103,14 +103,8 @@ function RSListToolbar({
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onDelete} onClick={onDelete}
/> />
<div className='px-1 py-1' id='items-table-help'> <HelpButton topic={HelpTopic.CSTLIST} offset={5} />
<HelpIcon color='text-primary' size={5} /> </Overlay>);
</div>
<ConceptTooltip anchorSelect='#items-table-help' offset={5}>
<HelpRSFormItems />
</ConceptTooltip>
</div>
</div>);
} }
export default RSListToolbar; export default RSListToolbar;

View File

@ -1,6 +1,7 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph'; import { GraphEdge, GraphNode, LayoutTypes } from 'reagraph';
import Overlay from '../../../components/Common/Overlay';
import InfoConstituenta from '../../../components/Shared/InfoConstituenta'; import InfoConstituenta from '../../../components/Shared/InfoConstituenta';
import SelectedCounter from '../../../components/Shared/SelectedCounter'; import SelectedCounter from '../../../components/Shared/SelectedCounter';
import { useRSForm } from '../../../context/RSFormContext'; import { useRSForm } from '../../../context/RSFormContext';
@ -225,15 +226,17 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
/> />
{hoverCst ? {hoverCst ?
<div className='relative'> <Overlay
position='top-[1.6rem] left-[2.6rem] px-3 w-[25rem] h-fit min-h-[11rem] overflow-y-auto'
layer='z-tooltip'
className='border shadow-md clr-app'
>
<InfoConstituenta <InfoConstituenta
data={hoverCst} data={hoverCst}
className='absolute top-[1.6rem] left-[2.6rem] z-tooltip w-[25rem] min-h-[11rem] shadow-md overflow-y-auto border h-fit clr-app px-3'
/> />
</div> : null} </Overlay> : null}
<div className='relative z-pop'> <Overlay position='top-0 left-0 max-w-[13.5rem] min-w-[13.5rem]' className='flex flex-col gap-3'>
<div className='absolute top-0 left-0 flex flex-col gap-3 max-w-[13.5rem] min-w-[13.5rem]'>
<GraphSidebar <GraphSidebar
coloring={coloringScheme} coloring={coloringScheme}
layout={layout} layout={layout}
@ -248,8 +251,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
toggleSelection={toggleDismissed} toggleSelection={toggleDismissed}
onEdit={onOpenEdit} onEdit={onOpenEdit}
/> />
</div> </Overlay>
</div>
<TermGraph <TermGraph
nodes={nodes} nodes={nodes}

View File

@ -18,7 +18,7 @@ function GraphSidebar({
layout, setLayout layout, setLayout
} : GraphSidebarProps) { } : GraphSidebarProps) {
return ( return (
<div className='flex flex-col px-2 pb-2 text-sm select-none mt-9 h-fit'> <div className='flex flex-col px-2 text-sm select-none mt-9 h-fit'>
<SelectSingle <SelectSingle
placeholder='Выберите цвет' placeholder='Выберите цвет'
options={SelectorGraphColoring} options={SelectorGraphColoring}

View File

@ -1,7 +1,8 @@
import ConceptTooltip from '../../../components/Common/ConceptTooltip' import MiniButton from '../../../components/Common/MiniButton';
import MiniButton from '../../../components/Common/MiniButton' import Overlay from '../../../components/Common/Overlay';
import HelpTermGraph from '../../../components/Help/HelpTermGraph' import HelpButton from '../../../components/Help/HelpButton';
import { ArrowsFocusIcon, DumpBinIcon, FilterIcon, HelpIcon, LetterAIcon, LetterALinesIcon, PlanetIcon, SmallPlusIcon } from '../../../components/Icons' import { ArrowsFocusIcon, DumpBinIcon, FilterIcon, LetterAIcon, LetterALinesIcon, PlanetIcon, SmallPlusIcon } from '../../../components/Icons';
import { HelpTopic } from '../../../models/miscelanious';
interface GraphToolbarProps { interface GraphToolbarProps {
isMutable: boolean isMutable: boolean
@ -28,8 +29,7 @@ function GraphToolbar({
onCreate, onDelete, onResetViewpoint onCreate, onDelete, onResetViewpoint
} : GraphToolbarProps) { } : GraphToolbarProps) {
return ( return (
<div className='relative w-full z-pop'> <Overlay position='w-full top-1 right-0 flex items-start justify-center'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'>
<MiniButton <MiniButton
tooltip='Настройки фильтрации узлов и связей' tooltip='Настройки фильтрации узлов и связей'
icon={<FilterIcon color='text-primary' size={5} />} icon={<FilterIcon color='text-primary' size={5} />}
@ -67,16 +67,8 @@ function GraphToolbar({
disabled={!is3D} disabled={!is3D}
onClick={toggleOrbit} onClick={toggleOrbit}
/> />
<div className='px-1 py-1' id='items-graph-help'> <HelpButton topic={HelpTopic.GRAPH_TERM} dimensions='max-w-[calc(100vw-20rem)]' offset={4} />
<HelpIcon color='text-primary' size={5} /> </Overlay>);
</div>
<ConceptTooltip anchorSelect='#items-graph-help' offset={4}>
<div className='text-sm max-w-[calc(100vw-20rem)]'>
<HelpTermGraph />
</div>
</ConceptTooltip>
</div>
</div>);
} }
export default GraphToolbar; export default GraphToolbar;

View File

@ -5,7 +5,7 @@ import { CstType, IRSForm } from '../../../models/rsform';
import { Graph } from '../../../utils/Graph'; import { Graph } from '../../../utils/Graph';
function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, toggleUpdate: boolean) { function useGraphFilter(schema: IRSForm | undefined, params: GraphFilterParams, toggleUpdate: boolean) {
const [ filtered, setFiltered ] = useState<Graph>(new Graph()); const [filtered, setFiltered] = useState<Graph>(new Graph());
const allowedTypes: CstType[] = useMemo( const allowedTypes: CstType[] = useMemo(
() => { () => {

View File

@ -8,8 +8,7 @@ function RSFormPage() {
return ( return (
<RSFormState schemaID={id ?? ''}> <RSFormState schemaID={id ?? ''}>
<RSTabs /> <RSTabs />
</RSFormState> </RSFormState>);
);
} }
export default RSFormPage; export default RSFormPage;

View File

@ -30,8 +30,8 @@ function RSTabsMenu({
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const { const {
isOwned, isMutable, isTracking, isReadonly, isClaimable, adminMode: isForceAdmin, isOwned, isMutable, isTracking, readerMode, isClaimable, adminMode,
toggleForceAdmin, toggleReadonly, processing toggleAdminMode, toggleReaderMode, processing
} = useRSForm(); } = useRSForm();
const schemaMenu = useDropdown(); const schemaMenu = useDropdown();
const editMenu = useDropdown(); const editMenu = useDropdown();
@ -147,15 +147,15 @@ function RSTabsMenu({
</DropdownButton> </DropdownButton>
{(isOwned || user?.is_staff) ? {(isOwned || user?.is_staff) ?
<DropdownCheckbox <DropdownCheckbox
value={isReadonly} value={readerMode}
setValue={toggleReadonly} setValue={toggleReaderMode}
label='Я — читатель!' label='Я — читатель!'
tooltip='Режим чтения' tooltip='Режим чтения'
/> : null} /> : null}
{user?.is_staff ? {user?.is_staff ?
<DropdownCheckbox <DropdownCheckbox
value={isForceAdmin} value={adminMode}
setValue={toggleForceAdmin} setValue={toggleAdminMode}
label='Я — администратор!' label='Я — администратор!'
tooltip='Режим редактирования для администраторов' tooltip='Режим редактирования для администраторов'
/> : null} /> : null}

View File

@ -68,9 +68,8 @@ function EditorPassword() {
}, [newPassword, oldPassword, newPasswordRepeat, setError]); }, [newPassword, oldPassword, newPasswordRepeat, setError]);
return ( return (
<div className='flex py-2 border-l-2 max-w-[14rem]'>
<form <form
className='flex flex-col justify-between px-6' className='flex flex-col justify-between px-6 border-l-2 max-w-[14rem] py-2'
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
@ -104,8 +103,7 @@ function EditorPassword() {
loading={loading} loading={loading}
/> />
</div> </div>
</form> </form>);
</div>);
} }
export default EditorPassword; export default EditorPassword;

View File

@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
import BackendError from '../../components/BackendError'; import BackendError from '../../components/BackendError';
import { ConceptLoader } from '../../components/Common/ConceptLoader'; import { ConceptLoader } from '../../components/Common/ConceptLoader';
import MiniButton from '../../components/Common/MiniButton'; import MiniButton from '../../components/Common/MiniButton';
import Overlay from '../../components/Common/Overlay';
import { NotSubscribedIcon,SubscribedIcon } from '../../components/Icons'; import { NotSubscribedIcon,SubscribedIcon } from '../../components/Icons';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
@ -24,14 +25,13 @@ function UserTabs() {
}, [auth, items]); }, [auth, items]);
return ( return (
<div className='w-full'> <>
{loading ? <ConceptLoader /> : null} {loading ? <ConceptLoader /> : null}
{error ? <BackendError error={error} /> : null} {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'> <Overlay position='mt-2 top-0 right-0'>
<div className='absolute top-0 right-0 mt-2'>
<MiniButton <MiniButton
tooltip='Показать/Скрыть список отслеживаний' tooltip='Показать/Скрыть список отслеживаний'
icon={showSubs icon={showSubs
@ -40,8 +40,7 @@ function UserTabs() {
} }
onClick={() => setShowSubs(prev => !prev)} onClick={() => setShowSubs(prev => !prev)}
/> />
</div> </Overlay>
</div>
<h1>Учетные данные пользователя</h1> <h1>Учетные данные пользователя</h1>
<div className='flex justify-center py-2 max-w-fit'> <div className='flex justify-center py-2 max-w-fit'>
<EditorProfile /> <EditorProfile />
@ -54,7 +53,7 @@ function UserTabs() {
<ViewSubscriptions items={subscriptions} /> <ViewSubscriptions items={subscriptions} />
</div> : null} </div> : null}
</div> : null} </div> : null}
</div>); </>);
} }
export default UserTabs; export default UserTabs;

View File

@ -2,15 +2,15 @@
* Module: CodeMirror customizations. * Module: CodeMirror customizations.
*/ */
import { syntaxTree } from '@codemirror/language' import { syntaxTree } from '@codemirror/language';
import { NodeType, Tree, TreeCursor } from '@lezer/common' import { NodeType, Tree, TreeCursor } from '@lezer/common';
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror' import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
import { IEntityReference, ISyntacticReference } from '../models/language' import { IEntityReference, ISyntacticReference } from '../models/language';
import { parseGrammemes } from '../models/languageAPI' import { parseGrammemes } from '../models/languageAPI';
import { IConstituenta } from '../models/rsform' import { IConstituenta } from '../models/rsform';
import { colorfgGrammeme,IColorTheme } from './color' import { colorfgGrammeme,IColorTheme } from './color';
import { describeConstituentaTerm, labelCstTypification, labelGrammeme } from './labels' import { describeConstituentaTerm, labelCstTypification, labelGrammeme } from './labels';
/** /**
* Represents syntax tree node data. * Represents syntax tree node data.

View File

@ -2,10 +2,10 @@
* Module: Single place for all color definitions in code (see index.css for full defs). * Module: Single place for all color definitions in code (see index.css for full defs).
*/ */
import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language' import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '../models/language';
import { GraphColoringScheme } from '../models/miscelanious' import { GraphColoringScheme } from '../models/miscelanious';
import { CstClass, ExpressionStatus, IConstituenta } from '../models/rsform' import { CstClass, ExpressionStatus, IConstituenta } from '../models/rsform';
import { ISyntaxTreeNode, TokenID } from '../models/rslang' import { ISyntaxTreeNode, TokenID } from '../models/rslang';
/** /**

View File

@ -482,7 +482,7 @@ export function labelSyntaxTree(node: ISyntaxTreeNode): string {
} }
export function labelGrammeme(gram: GramData): string { export function labelGrammeme(gram: GramData): string {
switch (gram) { switch (gram as Grammeme) {
default: return `Неизв: ${gram}`; default: return `Неизв: ${gram}`;
case Grammeme.NOUN: return 'ЧР: сущ'; case Grammeme.NOUN: return 'ЧР: сущ';