mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Refactor UI classes and improve editing UX
This commit is contained in:
parent
1009a2ec98
commit
6965e83e19
|
@ -33,7 +33,7 @@ function Root() {
|
|||
<Navigation />
|
||||
|
||||
<div id={globalIDs.main_scroll}
|
||||
className='overflow-x-auto overscroll-none'
|
||||
className='overscroll-none min-w-fit overflow-y-auto'
|
||||
style={{
|
||||
maxHeight: viewportHeight,
|
||||
overflowY: showScroll ? 'scroll': 'auto'
|
||||
|
@ -45,8 +45,7 @@ function Root() {
|
|||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</NavigationState>);
|
||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|||
import { IColorsProps, IControlProps } from './commonInterfaces';
|
||||
|
||||
interface ButtonProps
|
||||
extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'| 'type'> {
|
||||
extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title'| 'type'> {
|
||||
text?: string
|
||||
icon?: React.ReactNode
|
||||
|
||||
|
@ -12,11 +12,10 @@ extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonE
|
|||
}
|
||||
|
||||
function Button({
|
||||
text, icon, tooltip,
|
||||
text, icon, tooltip, loading,
|
||||
dense, disabled, noBorder, noOutline,
|
||||
colors = 'clr-btn-default',
|
||||
dimensions = 'w-fit h-fit',
|
||||
loading,
|
||||
className,
|
||||
...restProps
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
|
@ -36,7 +35,7 @@ function Button({
|
|||
'clr-outline': !noOutline,
|
||||
},
|
||||
colors,
|
||||
dimensions
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
|
|
|
@ -5,10 +5,9 @@ import { CheckboxCheckedIcon } from '../Icons';
|
|||
import Label from './Label';
|
||||
|
||||
export interface CheckboxProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'value' | 'onClick' > {
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title' | 'value' | 'onClick' > {
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
dimensions?: string
|
||||
tooltip?: string
|
||||
|
||||
value: boolean
|
||||
|
@ -17,7 +16,7 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
|
|||
|
||||
function Checkbox({
|
||||
id, disabled, tooltip, label,
|
||||
dimensions = 'w-fit', value, setValue, ...restProps
|
||||
className, value, setValue, ...restProps
|
||||
}: CheckboxProps) {
|
||||
const cursor = useMemo(
|
||||
() => {
|
||||
|
@ -44,8 +43,8 @@ function Checkbox({
|
|||
'flex items-center gap-2',
|
||||
'outline-none',
|
||||
'text-start',
|
||||
dimensions,
|
||||
cursor
|
||||
cursor,
|
||||
className
|
||||
)}
|
||||
title={tooltip}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -11,7 +11,7 @@ interface ConceptLoaderProps {
|
|||
export function ConceptLoader({size=10}: ConceptLoaderProps) {
|
||||
const {colors} = useConceptTheme();
|
||||
return (
|
||||
<div className='flex justify-center w-full h-full'>
|
||||
<div className='flex justify-center'>
|
||||
<ThreeDots
|
||||
color={colors.bgSelected}
|
||||
height={size*10}
|
||||
|
|
|
@ -21,7 +21,7 @@ function ConceptSearch({ value, onChange, noBorder, dimensions }: ConceptSearchP
|
|||
</Overlay>
|
||||
<TextInput noOutline
|
||||
placeholder='Поиск'
|
||||
dimensions='w-full pl-10'
|
||||
className='pl-10'
|
||||
noBorder={noBorder}
|
||||
value={value}
|
||||
onChange={event => (onChange ? onChange(event.target.value) : undefined)}
|
||||
|
|
|
@ -21,8 +21,7 @@ function DropdownCheckbox({ tooltip, setValue, disabled, ...restProps }: Dropdow
|
|||
!!setValue && !disabled && 'clr-hover'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
dimensions='w-full'
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
setValue={setValue}
|
||||
{...restProps}
|
||||
|
|
|
@ -71,7 +71,6 @@ function Modal({
|
|||
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full h-fit',
|
||||
'overflow-auto',
|
||||
className
|
||||
)}
|
||||
|
@ -84,7 +83,6 @@ function Modal({
|
|||
</div>
|
||||
|
||||
<div className={clsx(
|
||||
'w-full min-w-fit',
|
||||
'z-modal-controls',
|
||||
'px-6 py-3 flex gap-12 justify-center'
|
||||
)}>
|
||||
|
@ -92,14 +90,14 @@ function Modal({
|
|||
<Button autoFocus
|
||||
text={submitText}
|
||||
tooltip={!canSubmit ? submitInvalidTooltip: ''}
|
||||
dimensions='min-w-[8rem] min-h-[2.6rem]'
|
||||
className='min-w-[8rem] min-h-[2.6rem]'
|
||||
colors='clr-btn-primary'
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
/> : null}
|
||||
<Button
|
||||
text={readonly ? 'Закрыть' : 'Отмена'}
|
||||
dimensions='min-w-[8rem] min-h-[2.6rem]'
|
||||
className='min-w-[8rem] min-h-[2.6rem]'
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -6,12 +6,13 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title'
|
|||
tooltip?: string
|
||||
loading?: boolean
|
||||
icon?: React.ReactNode
|
||||
dimensions?: string
|
||||
}
|
||||
|
||||
function SubmitButton({
|
||||
text = 'ОК', icon, disabled, tooltip, loading, className,
|
||||
dimensions = 'w-fit h-fit', ...restProps
|
||||
text = 'ОК',
|
||||
icon, disabled, tooltip, loading,
|
||||
className,
|
||||
...restProps
|
||||
}: SubmitButtonProps) {
|
||||
return (
|
||||
<button type='submit'
|
||||
|
@ -23,7 +24,6 @@ function SubmitButton({
|
|||
'clr-btn-primary',
|
||||
'select-none disabled:cursor-not-allowed',
|
||||
loading && 'cursor-progress',
|
||||
dimensions,
|
||||
className
|
||||
)}
|
||||
disabled={disabled ?? loading}
|
||||
|
|
|
@ -5,24 +5,24 @@ import { IColorsProps, IEditorProps } from './commonInterfaces';
|
|||
import Label from './Label';
|
||||
|
||||
export interface TextAreaProps
|
||||
extends IEditorProps, IColorsProps, Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className' | 'title'> {
|
||||
extends IEditorProps, IColorsProps, Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'title'> {
|
||||
dense?: boolean
|
||||
}
|
||||
|
||||
function TextArea({
|
||||
id, label, required, tooltip, rows,
|
||||
dense, noBorder, noOutline,
|
||||
dimensions = 'w-full',
|
||||
className,
|
||||
colors = 'clr-input',
|
||||
...restProps
|
||||
}: TextAreaProps) {
|
||||
return (
|
||||
<div className={clsx(
|
||||
{
|
||||
'flex items-center gap-3': dense,
|
||||
'flex flex-col items-start gap-2': !dense
|
||||
'flex flex-col gap-2': !dense,
|
||||
'flex justify-stretch items-center gap-3': dense
|
||||
},
|
||||
dense && dimensions,
|
||||
dense && className,
|
||||
)}>
|
||||
<Label text={label} htmlFor={id} />
|
||||
<textarea id={id}
|
||||
|
@ -31,12 +31,12 @@ function TextArea({
|
|||
'px-3 py-2',
|
||||
'leading-tight',
|
||||
{
|
||||
'w-full': dense,
|
||||
'border': !noBorder,
|
||||
'flex-grow': dense,
|
||||
'clr-outline': !noOutline
|
||||
},
|
||||
colors,
|
||||
!dense && dimensions
|
||||
!dense && className
|
||||
)}
|
||||
rows={rows}
|
||||
required={required}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IColorsProps, IEditorProps } from './commonInterfaces';
|
|||
import Label from './Label';
|
||||
|
||||
interface TextInputProps
|
||||
extends IEditorProps, IColorsProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> {
|
||||
extends IEditorProps, IColorsProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'title'> {
|
||||
dense?: boolean
|
||||
allowEnter?: boolean
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
|
|||
|
||||
function TextInput({
|
||||
id, label, dense, tooltip, noBorder, noOutline, allowEnter, disabled,
|
||||
dimensions = 'w-full',
|
||||
className,
|
||||
colors = 'clr-input',
|
||||
onKeyDown,
|
||||
...restProps
|
||||
|
@ -25,10 +25,10 @@ function TextInput({
|
|||
return (
|
||||
<div className={clsx(
|
||||
{
|
||||
'flex flex-col items-start gap-2': !dense,
|
||||
'flex items-center gap-3': dense,
|
||||
'flex flex-col gap-2': !dense,
|
||||
'flex justify-stretch items-center gap-3': dense,
|
||||
},
|
||||
dense && dimensions
|
||||
dense && className
|
||||
)}>
|
||||
<Label text={label} htmlFor={id} />
|
||||
<input id={id}
|
||||
|
@ -38,12 +38,12 @@ function TextInput({
|
|||
'leading-tight truncate hover:text-clip',
|
||||
{
|
||||
'px-3': !noBorder || !disabled,
|
||||
'w-full': dense,
|
||||
'flex-grow': dense,
|
||||
'border': !noBorder,
|
||||
'clr-outline': !noOutline
|
||||
},
|
||||
colors,
|
||||
!dense && dimensions
|
||||
!dense && className
|
||||
)}
|
||||
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -14,7 +14,7 @@ extends Omit<CheckboxProps, 'value' | 'setValue'> {
|
|||
|
||||
function Tristate({
|
||||
id, disabled, tooltip, label,
|
||||
dimensions = 'w-fit',
|
||||
className,
|
||||
value, setValue,
|
||||
...restProps
|
||||
}: TristateProps) {
|
||||
|
@ -48,8 +48,8 @@ function Tristate({
|
|||
className={clsx(
|
||||
'flex items-center gap-2 text-start',
|
||||
'outline-none',
|
||||
dimensions,
|
||||
cursor
|
||||
cursor,
|
||||
className
|
||||
)}
|
||||
title={tooltip}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// =========== Module contains interfaces for common UI elements. ==========
|
||||
export interface IControlProps {
|
||||
tooltip?: string
|
||||
dimensions?: string
|
||||
|
||||
disabled?: boolean
|
||||
noBorder?: boolean
|
||||
noOutline?: boolean
|
||||
}
|
||||
|
||||
export interface IStylingProps {
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
}
|
||||
|
||||
export interface IEditorProps extends IControlProps {
|
||||
label?: string
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { IStylingProps } from '../Common/commonInterfaces';
|
||||
import DefaultNoData from './DefaultNoData';
|
||||
import PaginationTools from './PaginationTools';
|
||||
import TableBody from './TableBody';
|
||||
|
@ -24,13 +25,10 @@ export interface IConditionalStyle<TData> {
|
|||
}
|
||||
|
||||
export interface DataTableProps<TData extends RowData>
|
||||
extends Pick<TableOptions<TData>,
|
||||
extends IStylingProps, Pick<TableOptions<TData>,
|
||||
'data' | 'columns' |
|
||||
'onRowSelectionChange' | 'onColumnVisibilityChange'
|
||||
> {
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
|
||||
dense?: boolean
|
||||
headPosition?: string
|
||||
noHeader?: boolean
|
||||
|
|
|
@ -23,7 +23,7 @@ function Navigation () {
|
|||
return (
|
||||
<nav className={clsx(
|
||||
'z-navigation',
|
||||
'sticky top-0 left-0 right-0',
|
||||
'sticky top-0 left-0 right-0',
|
||||
'clr-app',
|
||||
'select-none'
|
||||
)}>
|
||||
|
|
|
@ -55,10 +55,11 @@ extends Pick<ReactCodeMirrorProps,
|
|||
noTooltip?: boolean
|
||||
innerref?: RefObject<ReactCodeMirrorRef> | undefined
|
||||
onChange?: (newValue: string) => void
|
||||
onAnalyze?: () => void
|
||||
}
|
||||
|
||||
function RSInput({
|
||||
id, label, innerref, onChange,
|
||||
id, label, innerref, onChange, onAnalyze,
|
||||
disabled, noTooltip,
|
||||
dimensions = 'w-full',
|
||||
...restProps
|
||||
|
@ -120,8 +121,12 @@ function RSInput({
|
|||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
|
||||
onAnalyze();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, [thisRef]);
|
||||
}, [thisRef, onAnalyze]);
|
||||
|
||||
return (
|
||||
<div className={clsx(
|
||||
|
|
|
@ -16,7 +16,8 @@ Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
|
|||
|
||||
function SelectGrammeme({
|
||||
value, setValue,
|
||||
dimensions, className, placeholder
|
||||
dimensions, className, placeholder,
|
||||
...restProps
|
||||
}: SelectGrammemeProps) {
|
||||
const [options, setOptions] = useState<IGrammemeOption[]>([]);
|
||||
|
||||
|
@ -37,6 +38,7 @@ function SelectGrammeme({
|
|||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
|
||||
{...restProps}
|
||||
/>);
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
|
|||
<TextInput
|
||||
label='Сокращение'
|
||||
value={alias}
|
||||
dimensions='max-w-sm'
|
||||
className='max-w-sm'
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
/>
|
||||
<TextArea
|
||||
|
|
|
@ -18,46 +18,48 @@ interface ConstituentaTabProps {
|
|||
function ConstituentaTab({state, partialUpdate}: ConstituentaTabProps) {
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex justify-center w-full gap-3 pr-2'>
|
||||
<div className='flex self-center gap-3 pr-2'>
|
||||
<SelectSingle
|
||||
className='min-w-[14rem] self-center'
|
||||
className='min-w-[14rem]'
|
||||
options={SelectorCstType}
|
||||
placeholder='Выберите тип'
|
||||
value={{ value: state.cst_type, label: labelCstType(state.cst_type) }}
|
||||
onChange={data => partialUpdate({ cst_type: data?.value ?? CstType.TERM})}
|
||||
/>
|
||||
<TextInput id='alias' label='Имя'
|
||||
dense
|
||||
dimensions='w-[7rem]'
|
||||
<TextInput dense
|
||||
label='Имя'
|
||||
className='w-[7rem]'
|
||||
value={state.alias}
|
||||
onChange={event => partialUpdate({ alias: event.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<TextArea id='term' label='Термин'
|
||||
<TextArea spellCheck
|
||||
label='Термин'
|
||||
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
|
||||
rows={2}
|
||||
value={state.term_raw}
|
||||
spellCheck
|
||||
onChange={event => partialUpdate({ term_raw: event.target.value })}
|
||||
/>
|
||||
<RSInput id='expression' label='Формальное определение'
|
||||
<RSInput
|
||||
label='Формальное определение'
|
||||
placeholder='Родоструктурное выражение, задающее формальное определение'
|
||||
height='5.1rem'
|
||||
value={state.definition_formal}
|
||||
onChange={value => partialUpdate({definition_formal: value})}
|
||||
/>
|
||||
<TextArea id='definition' label='Текстовое определение'
|
||||
<TextArea
|
||||
label='Текстовое определение'
|
||||
placeholder='Лингвистическая интерпретация формального выражения'
|
||||
rows={2}
|
||||
value={state.definition_raw}
|
||||
spellCheck
|
||||
onChange={event => partialUpdate({ definition_raw: event.target.value })}
|
||||
/>
|
||||
<TextArea id='convention' label='Конвенция / Комментарий'
|
||||
<TextArea spellCheck
|
||||
label='Конвенция / Комментарий'
|
||||
placeholder='Договоренность об интерпретации или пояснение к схеме'
|
||||
rows={2}
|
||||
value={state.convention}
|
||||
spellCheck
|
||||
onChange={event => partialUpdate({ convention: event.target.value })}
|
||||
/>
|
||||
</div>);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
|
@ -118,18 +119,21 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
onSubmit={handleSubmit}
|
||||
submitText='Создать'
|
||||
className='max-w-[43rem] min-w-[43rem] min-h-[36rem] px-6'
|
||||
>
|
||||
<Tabs defaultFocus forceRenderTabPanel
|
||||
selectedTabClassName='clr-selected'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={setActiveTab}
|
||||
>
|
||||
<Overlay position='top-0 right-[6rem]'>
|
||||
<HelpButton topic={HelpTopic.RSTEMPLATES} dimensions='max-w-[35rem]' />
|
||||
</Overlay>
|
||||
|
||||
<TabList className='flex justify-center mb-3'>
|
||||
<div className='flex border divide-x rounded-none w-fit'>
|
||||
<Tabs forceRenderTabPanel
|
||||
selectedTabClassName='clr-selected'
|
||||
className='flex flex-col'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={setActiveTab}
|
||||
>
|
||||
<TabList className={clsx(
|
||||
'mb-3 self-center',
|
||||
'flex',
|
||||
'border divide-x rounded-none'
|
||||
)}>
|
||||
<ConceptTab
|
||||
label='Шаблон'
|
||||
tooltip='Выбор шаблона выражения'
|
||||
|
@ -145,33 +149,30 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
|
|||
tooltip='Редактирование атрибутов конституенты'
|
||||
className='w-[8rem]'
|
||||
/>
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
<div className='w-full'>
|
||||
</TabList>
|
||||
|
||||
<TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '': 'none' }}>
|
||||
<TemplateTab
|
||||
state={template}
|
||||
partialUpdate={updateTemplate}
|
||||
/>
|
||||
<TemplateTab
|
||||
state={template}
|
||||
partialUpdate={updateTemplate}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel style={{ display: activeTab === TabID.ARGUMENTS ? '': 'none' }}>
|
||||
<ArgumentsTab
|
||||
schema={schema}
|
||||
state={substitutes}
|
||||
partialUpdate={updateSubstitutes}
|
||||
/>
|
||||
<ArgumentsTab
|
||||
schema={schema}
|
||||
state={substitutes}
|
||||
partialUpdate={updateSubstitutes}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel style={{ display: activeTab === TabID.CONSTITUENTA ? '': 'none' }}>
|
||||
<ConstituentaTab
|
||||
state={constituenta}
|
||||
partialUpdate={updateConstituenta}
|
||||
/>
|
||||
<ConstituentaTab
|
||||
state={constituenta}
|
||||
partialUpdate={updateConstituenta}
|
||||
/>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
|
|
|
@ -89,10 +89,10 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
|||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex justify-stretch'>
|
||||
<SelectSingle
|
||||
placeholder='Выберите категорию'
|
||||
className='w-full border-none'
|
||||
className='flex-grow border-none'
|
||||
options={categorySelector}
|
||||
value={state.filterCategory && selectedSchema ? {
|
||||
value: state.filterCategory.id,
|
||||
|
@ -103,7 +103,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
|
|||
/>
|
||||
<SelectSingle
|
||||
placeholder='Выберите источник'
|
||||
className='min-w-[15rem]'
|
||||
className='min-w-[12rem]'
|
||||
options={templateSelector}
|
||||
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
|
||||
onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})}
|
||||
|
|
|
@ -56,21 +56,21 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
|
|||
onSubmit={handleSubmit}
|
||||
submitText='Создать'
|
||||
className={clsx(
|
||||
'h-fit min-w-[35rem]',
|
||||
'py-2 px-6 flex flex-col gap-3 justify-stretch'
|
||||
'w-[35rem]',
|
||||
'py-2 px-6 flex flex-col gap-3'
|
||||
)}
|
||||
>
|
||||
<div className='flex justify-center w-full gap-6'>
|
||||
<div className='flex self-center gap-6'>
|
||||
<SelectSingle
|
||||
placeholder='Выберите тип'
|
||||
className='min-w-[15rem] self-center'
|
||||
className='min-w-[15rem]'
|
||||
options={SelectorCstType}
|
||||
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }}
|
||||
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})}
|
||||
/>
|
||||
<TextInput dense
|
||||
label='Имя'
|
||||
dimensions='w-[7rem]'
|
||||
className='w-[7rem]'
|
||||
value={cstData.alias}
|
||||
onChange={event => updateCstData({ alias: event.target.value})}
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
|
||||
|
@ -51,18 +52,22 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
|
|||
canSubmit={isValid}
|
||||
onSubmit={handleSubmit}
|
||||
className='min-w-[40rem] max-w-[40rem] px-6 min-h-[34rem]'
|
||||
>
|
||||
<Tabs defaultFocus
|
||||
selectedTabClassName='clr-selected'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={setActiveTab}
|
||||
>
|
||||
<Overlay position='top-0 right-[4rem]'>
|
||||
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[35rem]' offset={14} />
|
||||
</Overlay>
|
||||
|
||||
<TabList className='flex justify-center mb-3'>
|
||||
<div className='flex border divide-x rounded-none w-fit'>
|
||||
|
||||
<Tabs
|
||||
selectedTabClassName='clr-selected'
|
||||
className='flex flex-col'
|
||||
selectedIndex={activeTab}
|
||||
onSelect={setActiveTab}
|
||||
>
|
||||
<TabList className={clsx(
|
||||
'mb-3 self-center',
|
||||
'flex',
|
||||
'border divide-x rounded-none'
|
||||
)}>
|
||||
<ConceptTab
|
||||
tooltip='Отсылка на термин в заданной словоформе'
|
||||
label={labelReferenceType(ReferenceType.ENTITY)}
|
||||
|
@ -73,26 +78,25 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
|
|||
label={labelReferenceType(ReferenceType.SYNTACTIC)}
|
||||
className='w-[12rem]'
|
||||
/>
|
||||
</div>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<EntityTab
|
||||
initial={initial}
|
||||
items={items}
|
||||
setReference={setReference}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<EntityTab
|
||||
initial={initial}
|
||||
items={items}
|
||||
setReference={setReference}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<SyntacticTab
|
||||
initial={initial}
|
||||
setReference={setReference}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<TabPanel>
|
||||
<SyntacticTab
|
||||
initial={initial}
|
||||
setReference={setReference}
|
||||
setIsValid={setIsValid}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
|
|
|
@ -73,19 +73,19 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
|
|||
rows={8}
|
||||
/>
|
||||
|
||||
<div className='flex gap-6 flex-start'>
|
||||
<div className='flex gap-3'>
|
||||
<TextInput dense
|
||||
label='Отсылаемая конституента'
|
||||
label='Конституента'
|
||||
placeholder='Имя'
|
||||
dimensions='max-w-[17rem] min-w-[17rem]'
|
||||
className='max-w-[11rem] min-w-[11rem]'
|
||||
value={alias}
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
/>
|
||||
<TextInput disabled dense noBorder
|
||||
label='Термин'
|
||||
className='flex-grow text-sm'
|
||||
value={term}
|
||||
tooltip={term}
|
||||
dimensions='w-full text-sm'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -94,11 +94,10 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
|
|||
setSelected={setSelectedGrams}
|
||||
/>
|
||||
|
||||
<div className='flex items-center gap-4 flex-start'>
|
||||
<Label text='Отсылаемая словоформа'/>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Label text='Словоформа'/>
|
||||
<SelectGrammeme
|
||||
placeholder='Выберите граммемы'
|
||||
dimensions='h-full '
|
||||
className='flex-grow'
|
||||
menuPlacement='top'
|
||||
value={selectedGrams}
|
||||
|
|
|
@ -20,9 +20,8 @@ function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
|
|||
}, [setSelected]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center w-full text-sm'>
|
||||
<div className='flex flex-start'>
|
||||
{PremadeWordForms.slice(0, 6).map(
|
||||
<div className='text-sm'>
|
||||
{PremadeWordForms.slice(0, 12).map(
|
||||
(data, index) =>
|
||||
<WordformButton key={`${prefixes.wordform_list}${index}`}
|
||||
text={data.text} example={data.example} grams={data.grams}
|
||||
|
@ -30,18 +29,6 @@ function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
|
|||
onSelectGrams={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-start'>
|
||||
{PremadeWordForms.slice(6, 12).map(
|
||||
(data, index) =>
|
||||
<WordformButton key={`${prefixes.wordform_list}${index}`}
|
||||
text={data.text} example={data.example} grams={data.grams}
|
||||
isSelected={data.grams.every(gram => selected.find(item => item.value as Grammeme === gram))}
|
||||
onSelectGrams={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps)
|
|||
<div className='flex flex-col gap-2'>
|
||||
<TextInput type='number' dense
|
||||
label='Смещение'
|
||||
dimensions='max-w-[10rem]'
|
||||
className='max-w-[10rem]'
|
||||
value={offset}
|
||||
onChange={event => setOffset(event.target.valueAsNumber)}
|
||||
/>
|
||||
|
|
|
@ -15,7 +15,7 @@ function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...re
|
|||
<button type='button' tabIndex={-1}
|
||||
onClick={() => onSelectGrams(grams)}
|
||||
className={clsx(
|
||||
'min-w-[6rem]',
|
||||
'min-w-[6.15rem]',
|
||||
'p-1',
|
||||
'border rounded-none',
|
||||
'cursor-pointer',
|
||||
|
|
|
@ -127,7 +127,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
hideWindow={hideWindow}
|
||||
submitText='Сохранить'
|
||||
onSubmit={handleSubmit}
|
||||
className='min-w-[40rem] max-w-[40rem] px-6'
|
||||
className='flex flex-col min-w-[40rem] max-w-[40rem] px-6'
|
||||
>
|
||||
<Overlay position='top-[-0.2rem] left-[7.5rem]'>
|
||||
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
|
||||
|
@ -144,44 +144,41 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
<Label text='Параметры словоформы' />
|
||||
</div>
|
||||
|
||||
<div className='flex items-start justify-between w-full'>
|
||||
<div className='flex items-center'>
|
||||
<TextArea
|
||||
placeholder='Введите текст'
|
||||
dimensions='min-w-[20rem] w-full min-h-[5rem]'
|
||||
rows={2}
|
||||
value={inputText}
|
||||
onChange={event => setInputText(event.target.value)}
|
||||
<div className='flex justify-stretch'>
|
||||
<TextArea
|
||||
placeholder='Введите текст'
|
||||
className='min-w-[20rem] min-h-[5rem] flex-grow'
|
||||
rows={2}
|
||||
value={inputText}
|
||||
onChange={event => setInputText(event.target.value)}
|
||||
/>
|
||||
<div className='flex flex-col self-center gap-1'>
|
||||
<MiniButton noHover
|
||||
tooltip='Определить граммемы'
|
||||
icon={<BiRightArrow
|
||||
size='1.25rem'
|
||||
className={inputText ? 'clr-text-primary' : ''}
|
||||
/>}
|
||||
disabled={textProcessor.loading || !inputText}
|
||||
onClick={handleParse}
|
||||
/>
|
||||
<MiniButton noHover
|
||||
tooltip='Генерировать словоформу'
|
||||
icon={<BiLeftArrow size='1.25rem' className={inputGrams.length !== 0 ? 'clr-text-primary' : ''} />}
|
||||
disabled={textProcessor.loading || inputGrams.length == 0}
|
||||
onClick={handleInflect}
|
||||
/>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<MiniButton
|
||||
tooltip='Определить граммемы'
|
||||
icon={<BiRightArrow
|
||||
size='1.25rem'
|
||||
className={inputText ? 'clr-text-primary' : ''}
|
||||
/>}
|
||||
disabled={textProcessor.loading || !inputText}
|
||||
onClick={handleParse}
|
||||
/>
|
||||
<MiniButton
|
||||
tooltip='Генерировать словоформу'
|
||||
icon={<BiLeftArrow size='1.25rem' className={inputGrams.length !== 0 ? 'clr-text-primary' : ''} />}
|
||||
disabled={textProcessor.loading || inputGrams.length == 0}
|
||||
onClick={handleInflect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SelectGrammeme
|
||||
placeholder='Выберите граммемы'
|
||||
dimensions='min-w-[15rem] max-w-[15rem] h-full '
|
||||
className='flex-grow'
|
||||
dimensions='min-w-[15rem] max-w-[15rem]'
|
||||
value={inputGrams}
|
||||
setValue={setInputGrams}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Overlay position='top-2 left-0'>
|
||||
<MiniButton
|
||||
<MiniButton noHover
|
||||
tooltip='Внести словоформу'
|
||||
icon={<BiCheck
|
||||
size='1.25rem'
|
||||
|
@ -190,7 +187,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
|
||||
onClick={handleAddForm}
|
||||
/>
|
||||
<MiniButton
|
||||
<MiniButton noHover
|
||||
tooltip='Генерировать стандартные словоформы'
|
||||
icon={<BiChevronsDown size='1.25rem' className={inputText ? 'clr-text-primary' : ''}
|
||||
/>}
|
||||
|
@ -201,7 +198,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
|
|||
|
||||
<div className={clsx(
|
||||
'mt-3 mb-2',
|
||||
'flex justify-center items-center',
|
||||
'flex self-center items-center',
|
||||
'text-sm text-center font-semibold'
|
||||
)}>
|
||||
<span>Заданные вручную словоформы [{forms.length}]</span>
|
||||
|
|
|
@ -51,13 +51,13 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
|
|||
canSubmit={validated}
|
||||
onSubmit={handleSubmit}
|
||||
className={clsx(
|
||||
'w-full min-w-[24rem]',
|
||||
'w-[30rem]',
|
||||
'py-6 px-6 flex gap-6 justify-center items-center'
|
||||
)}
|
||||
>
|
||||
<SelectSingle
|
||||
placeholder='Выберите тип'
|
||||
className='min-w-[14rem] self-center'
|
||||
className='min-w-[16rem] self-center'
|
||||
options={SelectorCstType}
|
||||
value={{
|
||||
value: cstData.cst_type,
|
||||
|
@ -65,14 +65,12 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
|
|||
}}
|
||||
onChange={data => updateData({cst_type: data?.value ?? CstType.BASE})}
|
||||
/>
|
||||
<div>
|
||||
<TextInput dense
|
||||
label='Имя'
|
||||
dimensions='w-[7rem]'
|
||||
className='w-[7rem]'
|
||||
value={cstData.alias}
|
||||
onChange={event => updateData({alias: event.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
hideWindow={hideWindow}
|
||||
className='px-6'
|
||||
>
|
||||
<div className='w-full my-2 text-lg text-center'>
|
||||
<div className='my-2 text-lg text-center'>
|
||||
{!hoverNode ? expression : null}
|
||||
{hoverNode ?
|
||||
<div>
|
||||
|
@ -68,7 +68,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
<span>{expression.slice(hoverNode.finish)}</span>
|
||||
</div> : null}
|
||||
</div>
|
||||
<div className='w-full h-full overflow-auto'>
|
||||
<div
|
||||
className='relative'
|
||||
style={{
|
||||
|
@ -86,7 +85,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
|
|||
onNodePointerOut={handleHoverOut}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
|
|||
/>
|
||||
<Checkbox
|
||||
label='Загружать название и комментарий'
|
||||
dimensions='w-fit py-2'
|
||||
className='py-2'
|
||||
value={loadMetadata}
|
||||
setValue={value => setLoadMetadata(value)}
|
||||
/>
|
||||
|
|
|
@ -11,6 +11,37 @@ import { getCstExpressionPrefix } from '@/utils/misc';
|
|||
|
||||
const LOGIC_TYPIIFCATION = 'LOGIC';
|
||||
|
||||
function useCheckExpression({ schema }: { schema?: IRSForm }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
|
||||
|
||||
const resetParse = useCallback(() => setParseData(undefined), []);
|
||||
|
||||
function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
|
||||
setError(undefined);
|
||||
postCheckExpression(String(schema!.id), {
|
||||
data: { expression: expression },
|
||||
showError: true,
|
||||
setLoading,
|
||||
onError: error => setError(error),
|
||||
onSuccess: parse => {
|
||||
if (activeCst) {
|
||||
adjustResults(parse, expression.trim() === getCstExpressionPrefix(activeCst), activeCst.cst_type);
|
||||
}
|
||||
setParseData(parse);
|
||||
if (onSuccess) onSuccess(parse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { parseData, checkExpression, resetParse, error, setError, loading };
|
||||
}
|
||||
|
||||
export default useCheckExpression;
|
||||
|
||||
// ===== Internals ========
|
||||
|
||||
function checkTypeConsistency(type: CstType, typification: string, args: IArgumentInfo[]): boolean {
|
||||
switch (type) {
|
||||
case CstType.BASE:
|
||||
|
@ -45,6 +76,16 @@ function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstTyp
|
|||
position: 0
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (emptyExpression) {
|
||||
parse.parseResult = false;
|
||||
parse.errors.push({
|
||||
errorType: RSErrorType.globalEmptyDerived,
|
||||
isCritical: true,
|
||||
params: [],
|
||||
position: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!checkTypeConsistency(cstType, parse.typification, parse.args)) {
|
||||
parse.parseResult = false;
|
||||
|
@ -55,33 +96,4 @@ function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstTyp
|
|||
position: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function useCheckExpression({ schema }: { schema?: IRSForm }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
|
||||
|
||||
const resetParse = useCallback(() => setParseData(undefined), []);
|
||||
|
||||
function checkExpression(expression: string, activeCst?: IConstituenta, onSuccess?: DataCallback<IExpressionParse>) {
|
||||
setError(undefined);
|
||||
postCheckExpression(String(schema!.id), {
|
||||
data: { expression: expression },
|
||||
showError: true,
|
||||
setLoading,
|
||||
onError: error => setError(error),
|
||||
onSuccess: parse => {
|
||||
if (activeCst) {
|
||||
adjustResults(parse, expression === getCstExpressionPrefix(activeCst), activeCst.cst_type);
|
||||
}
|
||||
setParseData(parse);
|
||||
if (onSuccess) onSuccess(parse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { parseData, checkExpression, resetParse, error, setError, loading };
|
||||
}
|
||||
|
||||
export default useCheckExpression;
|
||||
}
|
|
@ -250,6 +250,7 @@ export enum RSErrorType {
|
|||
// !!!! Добавлены по сравнению с ConceptCore !!!!!
|
||||
globalNonemptyBase = 34855,
|
||||
globalUnexpectedType = 34856,
|
||||
globalEmptyDerived = 34857,
|
||||
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
|
|
@ -110,7 +110,7 @@ function CreateRSFormPage() {
|
|||
<TextInput required={!file}
|
||||
label='Сокращение'
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
dimensions='w-[14rem]'
|
||||
className='w-[14rem]'
|
||||
pattern={patterns.alias}
|
||||
tooltip={`не более ${limits.alias_len} символов`}
|
||||
value={alias}
|
||||
|
@ -131,11 +131,11 @@ function CreateRSFormPage() {
|
|||
<SubmitButton
|
||||
text='Создать схему'
|
||||
loading={processing}
|
||||
dimensions='min-w-[10rem]'
|
||||
className='min-w-[10rem]'
|
||||
/>
|
||||
<Button
|
||||
text='Отмена'
|
||||
dimensions='min-w-[10rem]'
|
||||
className='min-w-[10rem]'
|
||||
onClick={() => handleCancel()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -89,8 +89,7 @@ function LoginPage() {
|
|||
|
||||
<SubmitButton
|
||||
text='Войти'
|
||||
dimensions='w-[12rem] mt-3'
|
||||
className='self-center'
|
||||
className='self-center w-[12rem] mt-3'
|
||||
loading={loading}
|
||||
disabled={!username || !password}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { PiGraphLight } from "react-icons/pi";
|
||||
import { FaRegKeyboard } from 'react-icons/fa6';
|
||||
import { RiNodeTree } from 'react-icons/ri'
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import MiniButton from '@/components/Common/MiniButton';
|
||||
|
@ -12,14 +13,16 @@ import { RSTextWrapper } from '@/components/RSInput/textEditing';
|
|||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import DlgShowAST from '@/dialogs/DlgShowAST';
|
||||
import useCheckExpression from '@/hooks/useCheckExpression';
|
||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
||||
import { TokenID } from '@/models/rslang';
|
||||
import { labelTypification } from '@/utils/labels';
|
||||
import { getCstExpressionPrefix } from '@/utils/misc';
|
||||
|
||||
import RSAnalyzer from './RSAnalyzer';
|
||||
import ParsingResult from './ParsingResult';
|
||||
import RSEditorControls from './RSEditControls';
|
||||
import StatusBar from './StatusBar';
|
||||
|
||||
interface EditorRSExpressionProps {
|
||||
id?: string
|
||||
|
@ -46,6 +49,7 @@ function EditorRSExpression({
|
|||
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
||||
const [expression, setExpression] = useState('');
|
||||
const [showAST, setShowAST] = useState(false);
|
||||
const [showControls, setShowControls] = useLocalStorage('rseditor-show-controls', true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setIsModified(false);
|
||||
|
@ -132,35 +136,52 @@ function EditorRSExpression({
|
|||
/> : null}
|
||||
|
||||
<div>
|
||||
<Overlay position='top-[-0.375rem] left-[11rem]'>
|
||||
<Overlay position='top-0 right-0 flex'>
|
||||
<MiniButton noHover
|
||||
tooltip='Включение специальной клавиатуры'
|
||||
onClick={() => setShowControls(prev => !prev)}
|
||||
icon={<FaRegKeyboard size='1.25rem' className={showControls ? 'clr-text-primary': ''} />}
|
||||
/>
|
||||
<MiniButton noHover
|
||||
tooltip='Дерево разбора выражения'
|
||||
onClick={handleShowAST}
|
||||
icon={<PiGraphLight size='1.25rem' className='clr-text-primary' />}
|
||||
icon={<RiNodeTree size='1.25rem' className='clr-text-primary' />}
|
||||
/>
|
||||
</Overlay>
|
||||
</Overlay>
|
||||
|
||||
<Overlay position='top-[-0.5rem] right-1/2 translate-x-1/2'>
|
||||
<StatusBar
|
||||
processing={loading}
|
||||
isModified={isModified}
|
||||
constituenta={activeCst}
|
||||
parseData={parseData}
|
||||
onAnalyze={() => handleCheckExpression()}
|
||||
/>
|
||||
</Overlay>
|
||||
|
||||
<RSInput innerref={rsInput}
|
||||
value={value}
|
||||
minHeight='3.8rem'
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onAnalyze={handleCheckExpression}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
{showControls ?
|
||||
<RSEditorControls
|
||||
disabled={disabled}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
/> : null}
|
||||
|
||||
<RSAnalyzer
|
||||
parseData={parseData}
|
||||
processing={loading}
|
||||
isModified={isModified}
|
||||
activeCst={activeCst}
|
||||
onCheckExpression={handleCheckExpression}
|
||||
onShowError={onShowError}
|
||||
/>
|
||||
{(parseData && parseData.errors.length > 0) ?
|
||||
<div className='flex-grow text-sm border overflow-y-auto max-h-[4.5rem] min-h-[4.5rem]'>
|
||||
<ParsingResult
|
||||
data={parseData}
|
||||
disabled={disabled}
|
||||
onShowError={onShowError}
|
||||
/>
|
||||
</div> : null}
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ function ParsingResult({ data, disabled, onShowError }: ParsingResultProps) {
|
|||
const warningsCount = data.errors.length - errorCount;
|
||||
|
||||
return (
|
||||
<div className='px-2 py-1'>
|
||||
<div className='px-2 pt-1 text-sm'>
|
||||
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
|
||||
{data.errors.map(
|
||||
(error, index) => {
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import Button from '@/components/Common/Button';
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { IExpressionParse, IRSErrorDescription } from '@/models/rslang';
|
||||
|
||||
import ParsingResult from './ParsingResult';
|
||||
import StatusBar from './StatusBar';
|
||||
|
||||
interface RSAnalyzerProps {
|
||||
parseData?: IExpressionParse
|
||||
processing?: boolean
|
||||
|
||||
activeCst?: IConstituenta
|
||||
isModified: boolean
|
||||
disabled?: boolean
|
||||
|
||||
onCheckExpression: () => void
|
||||
onShowError: (error: IRSErrorDescription) => void
|
||||
}
|
||||
|
||||
function RSAnalyzer({
|
||||
parseData, processing,
|
||||
activeCst, disabled, isModified,
|
||||
onCheckExpression, onShowError,
|
||||
}: RSAnalyzerProps) {
|
||||
return (
|
||||
<div className='w-full max-h-[4.5rem] min-h-[4.5rem] flex'>
|
||||
<div className='flex flex-col'>
|
||||
<Button noOutline
|
||||
text='Проверить'
|
||||
tooltip='Проверить формальное определение'
|
||||
dimensions='w-[6.75rem] min-h-[3rem] z-pop rounded-none'
|
||||
colors='clr-btn-default'
|
||||
onClick={() => onCheckExpression()}
|
||||
/>
|
||||
<StatusBar
|
||||
isModified={isModified}
|
||||
constituenta={activeCst}
|
||||
parseData={parseData}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-full overflow-y-auto text-sm border rounded-none'>
|
||||
{processing ? <ConceptLoader size={6} /> : null}
|
||||
{(!processing && parseData) ?
|
||||
<ParsingResult
|
||||
data={parseData}
|
||||
disabled={disabled}
|
||||
onShowError={onShowError}
|
||||
/> : null}
|
||||
{(!processing && !parseData) ?
|
||||
<input disabled
|
||||
className='w-full px-2 py-1 text-base select-none h-fit clr-app'
|
||||
placeholder='Результаты проверки выражения'
|
||||
/> : null}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
|
||||
export default RSAnalyzer;
|
|
@ -85,54 +85,39 @@ interface RSEditorControlsProps {
|
|||
|
||||
function RSEditorControls({ onEdit, disabled }: RSEditorControlsProps) {
|
||||
return (
|
||||
<div className='flex items-center justify-start w-full text-sm'>
|
||||
<div className='w-fit'>
|
||||
<div className='flex justify-start'>
|
||||
{MAIN_FIRST_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
<div className='flex justify-start'>
|
||||
{MAIN_SECOND_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
<div className='flex justify-start'>
|
||||
{MAIN_THIRD_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-wrap text-sm divide-solid'>
|
||||
{MAIN_FIRST_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
{SECONDARY_FIRST_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
|
||||
<div className='w-fit'>
|
||||
<div className='flex justify-start'>
|
||||
{SECONDARY_FIRST_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
<div className='flex justify-start'>
|
||||
{SECONDARY_SECOND_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
<div className='flex justify-start'>
|
||||
{SECONDARY_THIRD_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
{MAIN_SECOND_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
{SECONDARY_SECOND_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
|
||||
{MAIN_THIRD_ROW.map(
|
||||
(token) =>
|
||||
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
|
||||
token={token} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
{SECONDARY_THIRD_ROW.map(
|
||||
({text, tooltip}) =>
|
||||
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
|
||||
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
|
||||
/>)}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps
|
|||
<button type='button' tabIndex={-1}
|
||||
disabled={disabled}
|
||||
title={tooltip}
|
||||
className='w-[2rem] h-6 cursor-pointer disabled:cursor-default border rounded-none clr-hover clr-btn-clear'
|
||||
className='w-[2rem] h-6 cursor-pointer disabled:cursor-default rounded-none clr-hover clr-btn-clear'
|
||||
onClick={() => onInsert(TokenID.ID_LOCAL, text)}
|
||||
>
|
||||
{text}
|
||||
|
|
|
@ -15,7 +15,7 @@ function RSTokenButton({ token, disabled, onInsert }: RSTokenButtonProps) {
|
|||
disabled={disabled}
|
||||
onClick={() => onInsert(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`}
|
||||
className={`px-1 cursor-pointer disabled:cursor-default h-6 ${width} outline-none clr-hover clr-btn-clear`}
|
||||
>
|
||||
{label ? <span className='whitespace-nowrap'>{label}</span> : null}
|
||||
</button>);
|
||||
|
|
|
@ -2,22 +2,26 @@
|
|||
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import { BiBug } from 'react-icons/bi';
|
||||
|
||||
import { ConceptLoader } from '@/components/Common/ConceptLoader';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { ExpressionStatus } from '@/models/rsform';
|
||||
import { type IConstituenta } from '@/models/rsform';
|
||||
import { inferStatus } from '@/models/rsformAPI';
|
||||
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
||||
import { colorbgCstStatus } from '@/utils/color';
|
||||
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels';
|
||||
import { labelExpressionStatus } from '@/utils/labels';
|
||||
|
||||
interface StatusBarProps {
|
||||
processing?: boolean
|
||||
isModified?: boolean
|
||||
parseData?: IExpressionParse
|
||||
constituenta?: IConstituenta
|
||||
onAnalyze: () => void
|
||||
}
|
||||
|
||||
function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
|
||||
function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze }: StatusBarProps) {
|
||||
const { colors } = useConceptTheme();
|
||||
const status = useMemo(() => {
|
||||
if (isModified) {
|
||||
|
@ -31,16 +35,28 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
|
|||
}, [isModified, constituenta, parseData]);
|
||||
|
||||
return (
|
||||
<div title={describeExpressionStatus(status)}
|
||||
<div
|
||||
title='Проверить определение [Ctrl + Q]'
|
||||
className={clsx(
|
||||
'h-full',
|
||||
'border rounded-none',
|
||||
'text-sm font-semibold small-caps text-center',
|
||||
'select-none'
|
||||
'w-[10rem] h-[1.75rem]',
|
||||
'px-3',
|
||||
'border',
|
||||
'select-none',
|
||||
'cursor-pointer',
|
||||
'duration-500 transition-colors'
|
||||
)}
|
||||
style={{backgroundColor: colorbgCstStatus(status, colors)}}
|
||||
style={{backgroundColor: processing ? colors.bgDefault : colorbgCstStatus(status, colors)}}
|
||||
onClick={onAnalyze}
|
||||
>
|
||||
{labelExpressionStatus(status)}
|
||||
{processing ?
|
||||
<ConceptLoader size={3} /> :
|
||||
<div className='flex items-center justify-center h-full gap-2'>
|
||||
<BiBug size='1rem' className='translate-y-[0.1rem]' />
|
||||
<span className='font-semibold small-caps'>
|
||||
{labelExpressionStatus(status)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ function FormRSForm({
|
|||
/>
|
||||
<TextInput required
|
||||
label='Сокращение'
|
||||
dimensions='w-[14rem]'
|
||||
className='w-[14rem]'
|
||||
pattern={patterns.alias}
|
||||
tooltip={`не более ${limits.alias_len} символов`}
|
||||
disabled={disabled}
|
||||
|
@ -107,7 +107,6 @@ function FormRSForm({
|
|||
<Checkbox
|
||||
label='Общедоступная схема'
|
||||
tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены'
|
||||
dimensions='w-fit'
|
||||
disabled={disabled}
|
||||
value={common}
|
||||
setValue={value => setCommon(value)}
|
||||
|
@ -115,7 +114,6 @@ function FormRSForm({
|
|||
<Checkbox
|
||||
label='Неизменная схема'
|
||||
tooltip='Только администраторы могут присваивать схемам неизменный статус'
|
||||
dimensions='w-fit'
|
||||
disabled={disabled || !user?.is_staff}
|
||||
value={canonical}
|
||||
setValue={value => setCanonical(value)}
|
||||
|
@ -123,8 +121,7 @@ function FormRSForm({
|
|||
</div>
|
||||
<SubmitButton
|
||||
text='Сохранить изменения'
|
||||
className='self-center'
|
||||
dimensions='my-2 w-fit'
|
||||
className='self-center my-2'
|
||||
loading={processing}
|
||||
disabled={!isModified || disabled}
|
||||
icon={<FiSave size='1.5rem' />}
|
||||
|
|
|
@ -128,7 +128,7 @@ function RSTable({
|
|||
<DataTable dense noFooter
|
||||
className={clsx(
|
||||
'min-h-[20rem]',
|
||||
'overflow-auto',
|
||||
'overflow-y-auto',
|
||||
'text-sm',
|
||||
'select-none'
|
||||
)}
|
||||
|
|
|
@ -111,7 +111,7 @@ function TermGraph({
|
|||
}, [noNavigation]);
|
||||
|
||||
return (
|
||||
<div className='w-full h-full overflow-auto outline-none'>
|
||||
<div className='outline-none'>
|
||||
<div className='relative' style={{width: canvasWidth, height: canvasHeight}}>
|
||||
<GraphUI
|
||||
draggable
|
||||
|
|
|
@ -68,7 +68,7 @@ function RSTabs() {
|
|||
cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate, resetAliases
|
||||
} = useRSForm();
|
||||
const { destroyItem } = useLibrary();
|
||||
const { setNoFooter, noNavigation } = useConceptTheme();
|
||||
const { setNoFooter } = useConceptTheme();
|
||||
const { user } = useAuth();
|
||||
const { mode, setMode } = useAccessMode();
|
||||
|
||||
|
@ -106,13 +106,6 @@ function RSTabs() {
|
|||
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
|
||||
const [showTemplates, setShowTemplates] = useState(false);
|
||||
|
||||
const panelHeight = useMemo(
|
||||
() => {
|
||||
return !noNavigation ?
|
||||
'calc(100vh - 4.8rem - 4px)'
|
||||
: 'calc(100vh - 2rem - 4px)';
|
||||
}, [noNavigation]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (schema) {
|
||||
const oldTitle = document.title;
|
||||
|
@ -403,11 +396,11 @@ function RSTabs() {
|
|||
onSelect={onSelectTab}
|
||||
defaultFocus
|
||||
selectedTabClassName='clr-selected'
|
||||
className='flex flex-col items-center min-w-[45rem]'
|
||||
className='flex flex-col min-w-[45rem]'
|
||||
>
|
||||
<TabList className={clsx(
|
||||
'h-[1.9rem]',
|
||||
'flex justify-stretch',
|
||||
'h-[1.9rem] mx-auto',
|
||||
'flex',
|
||||
'border-b-2 border-x-2 divide-x-2'
|
||||
)}>
|
||||
<RSTabsMenu isMutable={isMutable}
|
||||
|
@ -435,53 +428,51 @@ function RSTabs() {
|
|||
<ConceptTab label='Граф термов' />
|
||||
</TabList>
|
||||
|
||||
<div className='overflow-y-auto' style={{ maxHeight: panelHeight}}>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
|
||||
<EditorRSForm
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onToggleSubscribe={handleToggleSubscribe}
|
||||
onDownload={onDownloadSchema}
|
||||
onDestroy={onDestroySchema}
|
||||
onClaim={onClaimSchema}
|
||||
onShare={onShareSchema}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
|
||||
<EditorRSForm
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onToggleSubscribe={handleToggleSubscribe}
|
||||
onDownload={onDownloadSchema}
|
||||
onDestroy={onDestroySchema}
|
||||
onClaim={onClaimSchema}
|
||||
onShare={onShareSchema}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
|
||||
<EditorRSList
|
||||
isMutable={isMutable}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
|
||||
<EditorRSList
|
||||
isMutable={isMutable}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}>
|
||||
<EditorConstituenta
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
activeID={activeID}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
onRenameCst={promptRenameCst}
|
||||
onEditTerm={promptShowEditTerm}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}>
|
||||
<EditorConstituenta
|
||||
isMutable={isMutable}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
activeID={activeID}
|
||||
activeCst={activeCst}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
onRenameCst={promptRenameCst}
|
||||
onEditTerm={promptShowEditTerm}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}>
|
||||
<EditorTermGraph
|
||||
isMutable={isMutable}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
/>
|
||||
</TabPanel>
|
||||
</div>
|
||||
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}>
|
||||
<EditorTermGraph
|
||||
isMutable={isMutable}
|
||||
onOpenEdit={onOpenCst}
|
||||
onCreateCst={promptCreateCst}
|
||||
onDeleteCst={promptDeleteCst}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs> : null}
|
||||
</>);
|
||||
}
|
||||
|
|
|
@ -95,12 +95,12 @@ function RSTabsMenu({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-stretch h-full w-fit'>
|
||||
<div className='flex'>
|
||||
<div ref={schemaMenu.ref}>
|
||||
<Button noBorder dense tabIndex={-1}
|
||||
tooltip='Меню'
|
||||
icon={<BiMenu size='1.25rem' className='clr-text-controls' />}
|
||||
dimensions='h-full w-fit pl-2'
|
||||
className='h-full pl-2'
|
||||
style={{outlineColor: 'transparent'}}
|
||||
onClick={schemaMenu.toggle}
|
||||
/>
|
||||
|
@ -148,7 +148,7 @@ function RSTabsMenu({
|
|||
<div ref={editMenu.ref}>
|
||||
<Button dense noBorder tabIndex={-1}
|
||||
tooltip={'Редактирование'}
|
||||
dimensions='h-full w-fit'
|
||||
className='h-full'
|
||||
style={{outlineColor: 'transparent'}}
|
||||
icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>}
|
||||
onClick={editMenu.toggle}
|
||||
|
@ -173,7 +173,7 @@ function RSTabsMenu({
|
|||
<div ref={accessMenu.ref}>
|
||||
<Button dense noBorder tabIndex={-1}
|
||||
tooltip={`режим ${labelAccessMode(mode)}`}
|
||||
dimensions='h-full w-fit pr-2'
|
||||
className='h-full pr-2'
|
||||
style={{outlineColor: 'transparent'}}
|
||||
icon={
|
||||
mode === UserAccessMode.ADMIN ? <BiMeteor size='1.25rem' className='clr-text-primary'/>
|
||||
|
|
|
@ -93,18 +93,18 @@ function RegisterPage() {
|
|||
pattern={patterns.login}
|
||||
tooltip='Минимум 3 знака. Латинские буквы и цифры. Не может начинаться с цифры'
|
||||
value={username}
|
||||
dimensions='w-[15rem]'
|
||||
className='w-[15rem]'
|
||||
onChange={event => setUsername(event.target.value)}
|
||||
/>
|
||||
<TextInput id='password' type='password' required
|
||||
label='Пароль'
|
||||
dimensions='w-[15rem]'
|
||||
className='w-[15rem]'
|
||||
value={password}
|
||||
onChange={event => setPassword(event.target.value)}
|
||||
/>
|
||||
<TextInput id='password2' required type='password'
|
||||
label='Повторите пароль'
|
||||
dimensions='w-[15rem]'
|
||||
className='w-[15rem]'
|
||||
value={password2}
|
||||
onChange={event => setPassword2(event.target.value)}
|
||||
/>
|
||||
|
@ -145,13 +145,13 @@ function RegisterPage() {
|
|||
<div className='flex justify-around my-3'>
|
||||
<SubmitButton
|
||||
text='Регистрировать'
|
||||
dimensions='min-w-[10rem]'
|
||||
className='min-w-[10rem]'
|
||||
loading={loading}
|
||||
disabled={!acceptPrivacy}
|
||||
/>
|
||||
<Button
|
||||
text='Назад'
|
||||
dimensions='min-w-[10rem]'
|
||||
className='min-w-[10rem]'
|
||||
onClick={() => handleCancel()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -53,7 +53,7 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
|
|||
|
||||
return (
|
||||
<DataTable dense noFooter
|
||||
className='max-h-[23.8rem] overflow-auto text-sm border'
|
||||
className='max-h-[23.8rem] overflow-y-auto text-sm border'
|
||||
columns={columns}
|
||||
data={items}
|
||||
headPosition='0'
|
||||
|
|
|
@ -650,6 +650,8 @@ export function describeRSError(error: IRSErrorDescription): string {
|
|||
return `Непустое выражение базисного/константного множества`;
|
||||
case RSErrorType.globalUnexpectedType:
|
||||
return `Типизация выражения не соответствует типу конституенты`;
|
||||
case RSErrorType.globalEmptyDerived:
|
||||
return `Пустое выражение для выводимого понятия или утверждения`;
|
||||
}
|
||||
return 'UNKNOWN ERROR';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user