Refactor UI classes and improve editing UX

This commit is contained in:
IRBorisov 2023-12-18 12:25:39 +03:00
parent 1009a2ec98
commit 6965e83e19
49 changed files with 382 additions and 431 deletions

View File

@ -33,7 +33,7 @@ function Root() {
<Navigation /> <Navigation />
<div id={globalIDs.main_scroll} <div id={globalIDs.main_scroll}
className='overflow-x-auto overscroll-none' className='overscroll-none min-w-fit overflow-y-auto'
style={{ style={{
maxHeight: viewportHeight, maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto' overflowY: showScroll ? 'scroll': 'auto'
@ -45,7 +45,6 @@ function Root() {
> >
<Outlet /> <Outlet />
</main> </main>
<Footer /> <Footer />
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
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>, 'children' | 'title'| 'type'> {
text?: string text?: string
icon?: React.ReactNode icon?: React.ReactNode
@ -12,11 +12,10 @@ extends IControlProps, IColorsProps, Omit<React.ButtonHTMLAttributes<HTMLButtonE
} }
function Button({ function Button({
text, icon, tooltip, text, icon, tooltip, loading,
dense, disabled, noBorder, noOutline, dense, disabled, noBorder, noOutline,
colors = 'clr-btn-default', colors = 'clr-btn-default',
dimensions = 'w-fit h-fit', className,
loading,
...restProps ...restProps
}: ButtonProps) { }: ButtonProps) {
return ( return (
@ -36,7 +35,7 @@ function Button({
'clr-outline': !noOutline, 'clr-outline': !noOutline,
}, },
colors, colors,
dimensions className
)} )}
{...restProps} {...restProps}
> >

View File

@ -5,10 +5,9 @@ import { CheckboxCheckedIcon } from '../Icons';
import Label from './Label'; import Label from './Label';
export interface CheckboxProps export interface CheckboxProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title' | 'value' | 'onClick' > { extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title' | 'value' | 'onClick' > {
label?: string label?: string
disabled?: boolean disabled?: boolean
dimensions?: string
tooltip?: string tooltip?: string
value: boolean value: boolean
@ -17,7 +16,7 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
function Checkbox({ function Checkbox({
id, disabled, tooltip, label, id, disabled, tooltip, label,
dimensions = 'w-fit', value, setValue, ...restProps className, value, setValue, ...restProps
}: CheckboxProps) { }: CheckboxProps) {
const cursor = useMemo( const cursor = useMemo(
() => { () => {
@ -44,8 +43,8 @@ function Checkbox({
'flex items-center gap-2', 'flex items-center gap-2',
'outline-none', 'outline-none',
'text-start', 'text-start',
dimensions, cursor,
cursor className
)} )}
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}

View File

@ -11,7 +11,7 @@ interface ConceptLoaderProps {
export function ConceptLoader({size=10}: ConceptLoaderProps) { export function ConceptLoader({size=10}: ConceptLoaderProps) {
const {colors} = useConceptTheme(); const {colors} = useConceptTheme();
return ( return (
<div className='flex justify-center w-full h-full'> <div className='flex justify-center'>
<ThreeDots <ThreeDots
color={colors.bgSelected} color={colors.bgSelected}
height={size*10} height={size*10}

View File

@ -21,7 +21,7 @@ function ConceptSearch({ value, onChange, noBorder, dimensions }: ConceptSearchP
</Overlay> </Overlay>
<TextInput noOutline <TextInput noOutline
placeholder='Поиск' placeholder='Поиск'
dimensions='w-full pl-10' className='pl-10'
noBorder={noBorder} noBorder={noBorder}
value={value} value={value}
onChange={event => (onChange ? onChange(event.target.value) : undefined)} onChange={event => (onChange ? onChange(event.target.value) : undefined)}

View File

@ -22,7 +22,6 @@ function DropdownCheckbox({ tooltip, setValue, disabled, ...restProps }: Dropdow
)} )}
> >
<Checkbox <Checkbox
dimensions='w-full'
disabled={disabled} disabled={disabled}
setValue={setValue} setValue={setValue}
{...restProps} {...restProps}

View File

@ -71,7 +71,6 @@ function Modal({
<div <div
className={clsx( className={clsx(
'w-full h-fit',
'overflow-auto', 'overflow-auto',
className className
)} )}
@ -84,7 +83,6 @@ function Modal({
</div> </div>
<div className={clsx( <div className={clsx(
'w-full min-w-fit',
'z-modal-controls', 'z-modal-controls',
'px-6 py-3 flex gap-12 justify-center' 'px-6 py-3 flex gap-12 justify-center'
)}> )}>
@ -92,14 +90,14 @@ function Modal({
<Button autoFocus <Button autoFocus
text={submitText} text={submitText}
tooltip={!canSubmit ? submitInvalidTooltip: ''} tooltip={!canSubmit ? submitInvalidTooltip: ''}
dimensions='min-w-[8rem] min-h-[2.6rem]' className='min-w-[8rem] min-h-[2.6rem]'
colors='clr-btn-primary' colors='clr-btn-primary'
disabled={!canSubmit} disabled={!canSubmit}
onClick={handleSubmit} onClick={handleSubmit}
/> : null} /> : null}
<Button <Button
text={readonly ? 'Закрыть' : 'Отмена'} text={readonly ? 'Закрыть' : 'Отмена'}
dimensions='min-w-[8rem] min-h-[2.6rem]' className='min-w-[8rem] min-h-[2.6rem]'
onClick={handleCancel} onClick={handleCancel}
/> />
</div> </div>

View File

@ -6,12 +6,13 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title'
tooltip?: string tooltip?: string
loading?: boolean loading?: boolean
icon?: React.ReactNode icon?: React.ReactNode
dimensions?: string
} }
function SubmitButton({ function SubmitButton({
text = 'ОК', icon, disabled, tooltip, loading, className, text = 'ОК',
dimensions = 'w-fit h-fit', ...restProps icon, disabled, tooltip, loading,
className,
...restProps
}: SubmitButtonProps) { }: SubmitButtonProps) {
return ( return (
<button type='submit' <button type='submit'
@ -23,7 +24,6 @@ function SubmitButton({
'clr-btn-primary', 'clr-btn-primary',
'select-none disabled:cursor-not-allowed', 'select-none disabled:cursor-not-allowed',
loading && 'cursor-progress', loading && 'cursor-progress',
dimensions,
className className
)} )}
disabled={disabled ?? loading} disabled={disabled ?? loading}

View File

@ -5,24 +5,24 @@ import { IColorsProps, IEditorProps } from './commonInterfaces';
import Label from './Label'; import Label from './Label';
export interface TextAreaProps export interface TextAreaProps
extends IEditorProps, IColorsProps, Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className' | 'title'> { extends IEditorProps, IColorsProps, Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'title'> {
dense?: boolean dense?: boolean
} }
function TextArea({ function TextArea({
id, label, required, tooltip, rows, id, label, required, tooltip, rows,
dense, noBorder, noOutline, dense, noBorder, noOutline,
dimensions = 'w-full', className,
colors = 'clr-input', colors = 'clr-input',
...restProps ...restProps
}: TextAreaProps) { }: TextAreaProps) {
return ( return (
<div className={clsx( <div className={clsx(
{ {
'flex items-center gap-3': dense, 'flex flex-col gap-2': !dense,
'flex flex-col items-start gap-2': !dense 'flex justify-stretch items-center gap-3': dense
}, },
dense && dimensions, dense && className,
)}> )}>
<Label text={label} htmlFor={id} /> <Label text={label} htmlFor={id} />
<textarea id={id} <textarea id={id}
@ -31,12 +31,12 @@ function TextArea({
'px-3 py-2', 'px-3 py-2',
'leading-tight', 'leading-tight',
{ {
'w-full': dense,
'border': !noBorder, 'border': !noBorder,
'flex-grow': dense,
'clr-outline': !noOutline 'clr-outline': !noOutline
}, },
colors, colors,
!dense && dimensions !dense && className
)} )}
rows={rows} rows={rows}
required={required} required={required}

View File

@ -4,7 +4,7 @@ import { IColorsProps, IEditorProps } from './commonInterfaces';
import Label from './Label'; import Label from './Label';
interface TextInputProps interface TextInputProps
extends IEditorProps, IColorsProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> { extends IEditorProps, IColorsProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, 'title'> {
dense?: boolean dense?: boolean
allowEnter?: boolean allowEnter?: boolean
} }
@ -17,7 +17,7 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
function TextInput({ function TextInput({
id, label, dense, tooltip, noBorder, noOutline, allowEnter, disabled, id, label, dense, tooltip, noBorder, noOutline, allowEnter, disabled,
dimensions = 'w-full', className,
colors = 'clr-input', colors = 'clr-input',
onKeyDown, onKeyDown,
...restProps ...restProps
@ -25,10 +25,10 @@ function TextInput({
return ( return (
<div className={clsx( <div className={clsx(
{ {
'flex flex-col items-start gap-2': !dense, 'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense, 'flex justify-stretch items-center gap-3': dense,
}, },
dense && dimensions dense && className
)}> )}>
<Label text={label} htmlFor={id} /> <Label text={label} htmlFor={id} />
<input id={id} <input id={id}
@ -38,12 +38,12 @@ function TextInput({
'leading-tight truncate hover:text-clip', 'leading-tight truncate hover:text-clip',
{ {
'px-3': !noBorder || !disabled, 'px-3': !noBorder || !disabled,
'w-full': dense, 'flex-grow': dense,
'border': !noBorder, 'border': !noBorder,
'clr-outline': !noOutline 'clr-outline': !noOutline
}, },
colors, colors,
!dense && dimensions !dense && className
)} )}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown} onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
disabled={disabled} disabled={disabled}

View File

@ -14,7 +14,7 @@ extends Omit<CheckboxProps, 'value' | 'setValue'> {
function Tristate({ function Tristate({
id, disabled, tooltip, label, id, disabled, tooltip, label,
dimensions = 'w-fit', className,
value, setValue, value, setValue,
...restProps ...restProps
}: TristateProps) { }: TristateProps) {
@ -48,8 +48,8 @@ function Tristate({
className={clsx( className={clsx(
'flex items-center gap-2 text-start', 'flex items-center gap-2 text-start',
'outline-none', 'outline-none',
dimensions, cursor,
cursor className
)} )}
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}

View File

@ -1,13 +1,16 @@
// =========== Module contains interfaces for common UI elements. ========== // =========== Module contains interfaces for common UI elements. ==========
export interface IControlProps { export interface IControlProps {
tooltip?: string tooltip?: string
dimensions?: string
disabled?: boolean disabled?: boolean
noBorder?: boolean noBorder?: boolean
noOutline?: boolean noOutline?: boolean
} }
export interface IStylingProps {
style?: React.CSSProperties
className?: string
}
export interface IEditorProps extends IControlProps { export interface IEditorProps extends IControlProps {
label?: string label?: string
} }

View File

@ -10,6 +10,7 @@ import {
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { IStylingProps } from '../Common/commonInterfaces';
import DefaultNoData from './DefaultNoData'; import DefaultNoData from './DefaultNoData';
import PaginationTools from './PaginationTools'; import PaginationTools from './PaginationTools';
import TableBody from './TableBody'; import TableBody from './TableBody';
@ -24,13 +25,10 @@ export interface IConditionalStyle<TData> {
} }
export interface DataTableProps<TData extends RowData> export interface DataTableProps<TData extends RowData>
extends Pick<TableOptions<TData>, extends IStylingProps, Pick<TableOptions<TData>,
'data' | 'columns' | 'data' | 'columns' |
'onRowSelectionChange' | 'onColumnVisibilityChange' 'onRowSelectionChange' | 'onColumnVisibilityChange'
> { > {
style?: React.CSSProperties
className?: string
dense?: boolean dense?: boolean
headPosition?: string headPosition?: string
noHeader?: boolean noHeader?: boolean

View File

@ -55,10 +55,11 @@ extends Pick<ReactCodeMirrorProps,
noTooltip?: boolean noTooltip?: boolean
innerref?: RefObject<ReactCodeMirrorRef> | undefined innerref?: RefObject<ReactCodeMirrorRef> | undefined
onChange?: (newValue: string) => void onChange?: (newValue: string) => void
onAnalyze?: () => void
} }
function RSInput({ function RSInput({
id, label, innerref, onChange, id, label, innerref, onChange, onAnalyze,
disabled, noTooltip, disabled, noTooltip,
dimensions = 'w-full', dimensions = 'w-full',
...restProps ...restProps
@ -120,8 +121,12 @@ function RSInput({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
onAnalyze();
event.preventDefault();
event.stopPropagation();
} }
}, [thisRef]); }, [thisRef, onAnalyze]);
return ( return (
<div className={clsx( <div className={clsx(

View File

@ -16,7 +16,8 @@ Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
function SelectGrammeme({ function SelectGrammeme({
value, setValue, value, setValue,
dimensions, className, placeholder dimensions, className, placeholder,
...restProps
}: SelectGrammemeProps) { }: SelectGrammemeProps) {
const [options, setOptions] = useState<IGrammemeOption[]>([]); const [options, setOptions] = useState<IGrammemeOption[]>([]);
@ -37,6 +38,7 @@ function SelectGrammeme({
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))} onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
{...restProps}
/>); />);
} }

View File

@ -72,7 +72,7 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
<TextInput <TextInput
label='Сокращение' label='Сокращение'
value={alias} value={alias}
dimensions='max-w-sm' className='max-w-sm'
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextArea <TextArea

View File

@ -18,46 +18,48 @@ interface ConstituentaTabProps {
function ConstituentaTab({state, partialUpdate}: ConstituentaTabProps) { function ConstituentaTab({state, partialUpdate}: ConstituentaTabProps) {
return ( return (
<div className='flex flex-col gap-3'> <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 <SelectSingle
className='min-w-[14rem] self-center' className='min-w-[14rem]'
options={SelectorCstType} options={SelectorCstType}
placeholder='Выберите тип' placeholder='Выберите тип'
value={{ value: state.cst_type, label: labelCstType(state.cst_type) }} value={{ value: state.cst_type, label: labelCstType(state.cst_type) }}
onChange={data => partialUpdate({ cst_type: data?.value ?? CstType.TERM})} onChange={data => partialUpdate({ cst_type: data?.value ?? CstType.TERM})}
/> />
<TextInput id='alias' label='Имя' <TextInput dense
dense label='Имя'
dimensions='w-[7rem]' className='w-[7rem]'
value={state.alias} value={state.alias}
onChange={event => partialUpdate({ alias: event.target.value})} onChange={event => partialUpdate({ alias: event.target.value})}
/> />
</div> </div>
<TextArea id='term' label='Термин' <TextArea spellCheck
label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение' placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2} rows={2}
value={state.term_raw} value={state.term_raw}
spellCheck
onChange={event => partialUpdate({ term_raw: event.target.value })} onChange={event => partialUpdate({ term_raw: event.target.value })}
/> />
<RSInput id='expression' label='Формальное определение' <RSInput
label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
height='5.1rem' height='5.1rem'
value={state.definition_formal} value={state.definition_formal}
onChange={value => partialUpdate({definition_formal: value})} onChange={value => partialUpdate({definition_formal: value})}
/> />
<TextArea id='definition' label='Текстовое определение' <TextArea
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения' placeholder='Лингвистическая интерпретация формального выражения'
rows={2} rows={2}
value={state.definition_raw} value={state.definition_raw}
spellCheck spellCheck
onChange={event => partialUpdate({ definition_raw: event.target.value })} onChange={event => partialUpdate({ definition_raw: event.target.value })}
/> />
<TextArea id='convention' label='Конвенция / Комментарий' <TextArea spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение к схеме' placeholder='Договоренность об интерпретации или пояснение к схеме'
rows={2} rows={2}
value={state.convention} value={state.convention}
spellCheck
onChange={event => partialUpdate({ convention: event.target.value })} onChange={event => partialUpdate({ convention: event.target.value })}
/> />
</div>); </div>);

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
@ -118,18 +119,21 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'
className='max-w-[43rem] min-w-[43rem] min-h-[36rem] px-6' 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]'> <Overlay position='top-0 right-[6rem]'>
<HelpButton topic={HelpTopic.RSTEMPLATES} dimensions='max-w-[35rem]' /> <HelpButton topic={HelpTopic.RSTEMPLATES} dimensions='max-w-[35rem]' />
</Overlay> </Overlay>
<Tabs forceRenderTabPanel
<TabList className='flex justify-center mb-3'> selectedTabClassName='clr-selected'
<div className='flex border divide-x rounded-none w-fit'> className='flex flex-col'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<TabList className={clsx(
'mb-3 self-center',
'flex',
'border divide-x rounded-none'
)}>
<ConceptTab <ConceptTab
label='Шаблон' label='Шаблон'
tooltip='Выбор шаблона выражения' tooltip='Выбор шаблона выражения'
@ -145,10 +149,8 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
tooltip='Редактирование атрибутов конституенты' tooltip='Редактирование атрибутов конституенты'
className='w-[8rem]' className='w-[8rem]'
/> />
</div>
</TabList> </TabList>
<div className='w-full'>
<TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '': 'none' }}> <TabPanel style={{ display: activeTab === TabID.TEMPLATE ? '': 'none' }}>
<TemplateTab <TemplateTab
state={template} state={template}
@ -170,7 +172,6 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
partialUpdate={updateConstituenta} partialUpdate={updateConstituenta}
/> />
</TabPanel> </TabPanel>
</div>
</Tabs> </Tabs>
</Modal>); </Modal>);
} }

View File

@ -89,10 +89,10 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
return ( return (
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<div> <div>
<div className='flex justify-between'> <div className='flex justify-stretch'>
<SelectSingle <SelectSingle
placeholder='Выберите категорию' placeholder='Выберите категорию'
className='w-full border-none' className='flex-grow border-none'
options={categorySelector} options={categorySelector}
value={state.filterCategory && selectedSchema ? { value={state.filterCategory && selectedSchema ? {
value: state.filterCategory.id, value: state.filterCategory.id,
@ -103,7 +103,7 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
/> />
<SelectSingle <SelectSingle
placeholder='Выберите источник' placeholder='Выберите источник'
className='min-w-[15rem]' className='min-w-[12rem]'
options={templateSelector} options={templateSelector}
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null} value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})} onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})}

View File

@ -56,21 +56,21 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Создать' submitText='Создать'
className={clsx( className={clsx(
'h-fit min-w-[35rem]', 'w-[35rem]',
'py-2 px-6 flex flex-col gap-3 justify-stretch' '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 <SelectSingle
placeholder='Выберите тип' placeholder='Выберите тип'
className='min-w-[15rem] self-center' className='min-w-[15rem]'
options={SelectorCstType} options={SelectorCstType}
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }} value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }}
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})} onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})}
/> />
<TextInput dense <TextInput dense
label='Имя' label='Имя'
dimensions='w-[7rem]' className='w-[7rem]'
value={cstData.alias} value={cstData.alias}
onChange={event => updateCstData({ alias: event.target.value})} onChange={event => updateCstData({ alias: event.target.value})}
/> />

View File

@ -1,5 +1,6 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
@ -51,18 +52,22 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='min-w-[40rem] max-w-[40rem] px-6 min-h-[34rem]' 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]'> <Overlay position='top-0 right-[4rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[35rem]' offset={14} /> <HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[35rem]' offset={14} />
</Overlay> </Overlay>
<TabList className='flex justify-center mb-3'> <Tabs
<div className='flex border divide-x rounded-none w-fit'> 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 <ConceptTab
tooltip='Отсылка на термин в заданной словоформе' tooltip='Отсылка на термин в заданной словоформе'
label={labelReferenceType(ReferenceType.ENTITY)} label={labelReferenceType(ReferenceType.ENTITY)}
@ -73,7 +78,6 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
label={labelReferenceType(ReferenceType.SYNTACTIC)} label={labelReferenceType(ReferenceType.SYNTACTIC)}
className='w-[12rem]' className='w-[12rem]'
/> />
</div>
</TabList> </TabList>
<TabPanel> <TabPanel>

View File

@ -73,19 +73,19 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
rows={8} rows={8}
/> />
<div className='flex gap-6 flex-start'> <div className='flex gap-3'>
<TextInput dense <TextInput dense
label='Отсылаемая конституента' label='Конституента'
placeholder='Имя' placeholder='Имя'
dimensions='max-w-[17rem] min-w-[17rem]' className='max-w-[11rem] min-w-[11rem]'
value={alias} value={alias}
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextInput disabled dense noBorder <TextInput disabled dense noBorder
label='Термин' label='Термин'
className='flex-grow text-sm'
value={term} value={term}
tooltip={term} tooltip={term}
dimensions='w-full text-sm'
/> />
</div> </div>
@ -94,11 +94,10 @@ function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps)
setSelected={setSelectedGrams} setSelected={setSelectedGrams}
/> />
<div className='flex items-center gap-4 flex-start'> <div className='flex items-center gap-4'>
<Label text='Отсылаемая словоформа'/> <Label text='Словоформа'/>
<SelectGrammeme <SelectGrammeme
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
dimensions='h-full '
className='flex-grow' className='flex-grow'
menuPlacement='top' menuPlacement='top'
value={selectedGrams} value={selectedGrams}

View File

@ -20,9 +20,8 @@ function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
}, [setSelected]); }, [setSelected]);
return ( return (
<div className='flex flex-col items-center w-full text-sm'> <div className='text-sm'>
<div className='flex flex-start'> {PremadeWordForms.slice(0, 12).map(
{PremadeWordForms.slice(0, 6).map(
(data, index) => (data, index) =>
<WordformButton key={`${prefixes.wordform_list}${index}`} <WordformButton key={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams} text={data.text} example={data.example} grams={data.grams}
@ -30,18 +29,6 @@ function SelectWordForm({ selected, setSelected }: SelectWordFormProps) {
onSelectGrams={handleSelect} 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>); </div>);
} }

View File

@ -49,7 +49,7 @@ function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps)
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<TextInput type='number' dense <TextInput type='number' dense
label='Смещение' label='Смещение'
dimensions='max-w-[10rem]' className='max-w-[10rem]'
value={offset} value={offset}
onChange={event => setOffset(event.target.valueAsNumber)} onChange={event => setOffset(event.target.valueAsNumber)}
/> />

View File

@ -15,7 +15,7 @@ function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...re
<button type='button' tabIndex={-1} <button type='button' tabIndex={-1}
onClick={() => onSelectGrams(grams)} onClick={() => onSelectGrams(grams)}
className={clsx( className={clsx(
'min-w-[6rem]', 'min-w-[6.15rem]',
'p-1', 'p-1',
'border rounded-none', 'border rounded-none',
'cursor-pointer', 'cursor-pointer',

View File

@ -127,7 +127,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
hideWindow={hideWindow} hideWindow={hideWindow}
submitText='Сохранить' submitText='Сохранить'
onSubmit={handleSubmit} 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]'> <Overlay position='top-[-0.2rem] left-[7.5rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} /> <HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
@ -144,17 +144,16 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<Label text='Параметры словоформы' /> <Label text='Параметры словоформы' />
</div> </div>
<div className='flex items-start justify-between w-full'> <div className='flex justify-stretch'>
<div className='flex items-center'>
<TextArea <TextArea
placeholder='Введите текст' placeholder='Введите текст'
dimensions='min-w-[20rem] w-full min-h-[5rem]' className='min-w-[20rem] min-h-[5rem] flex-grow'
rows={2} rows={2}
value={inputText} value={inputText}
onChange={event => setInputText(event.target.value)} onChange={event => setInputText(event.target.value)}
/> />
<div className='flex flex-col gap-1'> <div className='flex flex-col self-center gap-1'>
<MiniButton <MiniButton noHover
tooltip='Определить граммемы' tooltip='Определить граммемы'
icon={<BiRightArrow icon={<BiRightArrow
size='1.25rem' size='1.25rem'
@ -163,25 +162,23 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleParse} onClick={handleParse}
/> />
<MiniButton <MiniButton noHover
tooltip='Генерировать словоформу' tooltip='Генерировать словоформу'
icon={<BiLeftArrow size='1.25rem' className={inputGrams.length !== 0 ? 'clr-text-primary' : ''} />} icon={<BiLeftArrow size='1.25rem' className={inputGrams.length !== 0 ? 'clr-text-primary' : ''} />}
disabled={textProcessor.loading || inputGrams.length == 0} disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect} onClick={handleInflect}
/> />
</div> </div>
</div>
<SelectGrammeme <SelectGrammeme
placeholder='Выберите граммемы' placeholder='Выберите граммемы'
dimensions='min-w-[15rem] max-w-[15rem] h-full ' dimensions='min-w-[15rem] max-w-[15rem]'
className='flex-grow'
value={inputGrams} value={inputGrams}
setValue={setInputGrams} setValue={setInputGrams}
/> />
</div> </div>
<Overlay position='top-2 left-0'> <Overlay position='top-2 left-0'>
<MiniButton <MiniButton noHover
tooltip='Внести словоформу' tooltip='Внести словоформу'
icon={<BiCheck icon={<BiCheck
size='1.25rem' size='1.25rem'
@ -190,7 +187,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
disabled={textProcessor.loading || !inputText || inputGrams.length == 0} disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton noHover
tooltip='Генерировать стандартные словоформы' tooltip='Генерировать стандартные словоформы'
icon={<BiChevronsDown size='1.25rem' className={inputText ? 'clr-text-primary' : ''} icon={<BiChevronsDown size='1.25rem' className={inputText ? 'clr-text-primary' : ''}
/>} />}
@ -201,7 +198,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<div className={clsx( <div className={clsx(
'mt-3 mb-2', 'mt-3 mb-2',
'flex justify-center items-center', 'flex self-center items-center',
'text-sm text-center font-semibold' 'text-sm text-center font-semibold'
)}> )}>
<span>Заданные вручную словоформы [{forms.length}]</span> <span>Заданные вручную словоформы [{forms.length}]</span>

View File

@ -51,13 +51,13 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx( className={clsx(
'w-full min-w-[24rem]', 'w-[30rem]',
'py-6 px-6 flex gap-6 justify-center items-center' 'py-6 px-6 flex gap-6 justify-center items-center'
)} )}
> >
<SelectSingle <SelectSingle
placeholder='Выберите тип' placeholder='Выберите тип'
className='min-w-[14rem] self-center' className='min-w-[16rem] self-center'
options={SelectorCstType} options={SelectorCstType}
value={{ value={{
value: cstData.cst_type, value: cstData.cst_type,
@ -65,14 +65,12 @@ 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>
<TextInput dense <TextInput dense
label='Имя' label='Имя'
dimensions='w-[7rem]' className='w-[7rem]'
value={cstData.alias} value={cstData.alias}
onChange={event => updateData({alias: event.target.value})} onChange={event => updateData({alias: event.target.value})}
/> />
</div>
</Modal>); </Modal>);
} }

View File

@ -59,7 +59,7 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
hideWindow={hideWindow} hideWindow={hideWindow}
className='px-6' 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 ? expression : null}
{hoverNode ? {hoverNode ?
<div> <div>
@ -68,7 +68,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
<span>{expression.slice(hoverNode.finish)}</span> <span>{expression.slice(hoverNode.finish)}</span>
</div> : null} </div> : null}
</div> </div>
<div className='w-full h-full overflow-auto'>
<div <div
className='relative' className='relative'
style={{ style={{
@ -86,7 +85,6 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
onNodePointerOut={handleHoverOut} onNodePointerOut={handleHoverOut}
/> />
</div> </div>
</div>
</Modal>); </Modal>);
} }

View File

@ -56,7 +56,7 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
/> />
<Checkbox <Checkbox
label='Загружать название и комментарий' label='Загружать название и комментарий'
dimensions='w-fit py-2' className='py-2'
value={loadMetadata} value={loadMetadata}
setValue={value => setLoadMetadata(value)} setValue={value => setLoadMetadata(value)}
/> />

View File

@ -11,6 +11,37 @@ import { getCstExpressionPrefix } from '@/utils/misc';
const LOGIC_TYPIIFCATION = 'LOGIC'; 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 { function checkTypeConsistency(type: CstType, typification: string, args: IArgumentInfo[]): boolean {
switch (type) { switch (type) {
case CstType.BASE: case CstType.BASE:
@ -45,6 +76,16 @@ function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstTyp
position: 0 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)) { if (!checkTypeConsistency(cstType, parse.typification, parse.args)) {
parse.parseResult = false; parse.parseResult = false;
@ -56,32 +97,3 @@ function adjustResults(parse: IExpressionParse, emptyExpression: boolean, cstTyp
}); });
} }
} }
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;

View File

@ -250,6 +250,7 @@ export enum RSErrorType {
// !!!! Добавлены по сравнению с ConceptCore !!!!! // !!!! Добавлены по сравнению с ConceptCore !!!!!
globalNonemptyBase = 34855, globalNonemptyBase = 34855,
globalUnexpectedType = 34856, globalUnexpectedType = 34856,
globalEmptyDerived = 34857,
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View File

@ -110,7 +110,7 @@ function CreateRSFormPage() {
<TextInput required={!file} <TextInput required={!file}
label='Сокращение' label='Сокращение'
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
dimensions='w-[14rem]' className='w-[14rem]'
pattern={patterns.alias} pattern={patterns.alias}
tooltip={`не более ${limits.alias_len} символов`} tooltip={`не более ${limits.alias_len} символов`}
value={alias} value={alias}
@ -131,11 +131,11 @@ function CreateRSFormPage() {
<SubmitButton <SubmitButton
text='Создать схему' text='Создать схему'
loading={processing} loading={processing}
dimensions='min-w-[10rem]' className='min-w-[10rem]'
/> />
<Button <Button
text='Отмена' text='Отмена'
dimensions='min-w-[10rem]' className='min-w-[10rem]'
onClick={() => handleCancel()} onClick={() => handleCancel()}
/> />
</div> </div>

View File

@ -89,8 +89,7 @@ function LoginPage() {
<SubmitButton <SubmitButton
text='Войти' text='Войти'
dimensions='w-[12rem] mt-3' className='self-center w-[12rem] mt-3'
className='self-center'
loading={loading} loading={loading}
disabled={!username || !password} disabled={!username || !password}
/> />

View File

@ -2,7 +2,8 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useLayoutEffect, useRef, useState } from 'react'; 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 { toast } from 'react-toastify';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
@ -12,14 +13,16 @@ import { RSTextWrapper } from '@/components/RSInput/textEditing';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import DlgShowAST from '@/dialogs/DlgShowAST'; import DlgShowAST from '@/dialogs/DlgShowAST';
import useCheckExpression from '@/hooks/useCheckExpression'; import useCheckExpression from '@/hooks/useCheckExpression';
import useLocalStorage from '@/hooks/useLocalStorage';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang'; import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
import { TokenID } from '@/models/rslang'; import { TokenID } from '@/models/rslang';
import { labelTypification } from '@/utils/labels'; import { labelTypification } from '@/utils/labels';
import { getCstExpressionPrefix } from '@/utils/misc'; import { getCstExpressionPrefix } from '@/utils/misc';
import RSAnalyzer from './RSAnalyzer'; import ParsingResult from './ParsingResult';
import RSEditorControls from './RSEditControls'; import RSEditorControls from './RSEditControls';
import StatusBar from './StatusBar';
interface EditorRSExpressionProps { interface EditorRSExpressionProps {
id?: string id?: string
@ -46,6 +49,7 @@ function EditorRSExpression({
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]); const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
const [expression, setExpression] = useState(''); const [expression, setExpression] = useState('');
const [showAST, setShowAST] = useState(false); const [showAST, setShowAST] = useState(false);
const [showControls, setShowControls] = useLocalStorage('rseditor-show-controls', true);
useLayoutEffect(() => { useLayoutEffect(() => {
setIsModified(false); setIsModified(false);
@ -132,11 +136,26 @@ function EditorRSExpression({
/> : null} /> : null}
<div> <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 <MiniButton noHover
tooltip='Дерево разбора выражения' tooltip='Дерево разбора выражения'
onClick={handleShowAST} onClick={handleShowAST}
icon={<PiGraphLight size='1.25rem' className='clr-text-primary' />} icon={<RiNodeTree size='1.25rem' className='clr-text-primary' />}
/>
</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> </Overlay>
@ -145,22 +164,24 @@ function EditorRSExpression({
minHeight='3.8rem' minHeight='3.8rem'
disabled={disabled} disabled={disabled}
onChange={handleChange} onChange={handleChange}
onAnalyze={handleCheckExpression}
{...restProps} {...restProps}
/> />
{showControls ?
<RSEditorControls <RSEditorControls
disabled={disabled} disabled={disabled}
onEdit={handleEdit} onEdit={handleEdit}
/> /> : null}
<RSAnalyzer {(parseData && parseData.errors.length > 0) ?
parseData={parseData} <div className='flex-grow text-sm border overflow-y-auto max-h-[4.5rem] min-h-[4.5rem]'>
processing={loading} <ParsingResult
isModified={isModified} data={parseData}
activeCst={activeCst} disabled={disabled}
onCheckExpression={handleCheckExpression}
onShowError={onShowError} onShowError={onShowError}
/> />
</div> : null}
</div> </div>
</>); </>);
} }

View File

@ -15,7 +15,7 @@ function ParsingResult({ data, disabled, onShowError }: ParsingResultProps) {
const warningsCount = data.errors.length - errorCount; const warningsCount = data.errors.length - errorCount;
return ( return (
<div className='px-2 py-1'> <div className='px-2 pt-1 text-sm'>
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p> <p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
{data.errors.map( {data.errors.map(
(error, index) => { (error, index) => {

View File

@ -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;

View File

@ -85,54 +85,39 @@ interface RSEditorControlsProps {
function RSEditorControls({ onEdit, disabled }: RSEditorControlsProps) { function RSEditorControls({ onEdit, disabled }: RSEditorControlsProps) {
return ( return (
<div className='flex items-center justify-start w-full text-sm'> <div className='flex-wrap text-sm divide-solid'>
<div className='w-fit'>
<div className='flex justify-start'>
{MAIN_FIRST_ROW.map( {MAIN_FIRST_ROW.map(
(token) => (token) =>
<RSTokenButton key={`${prefixes.rsedit_btn}${token}`} <RSTokenButton key={`${prefixes.rsedit_btn}${token}`}
token={token} onInsert={onEdit} disabled={disabled} 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='w-fit'>
<div className='flex justify-start'>
{SECONDARY_FIRST_ROW.map( {SECONDARY_FIRST_ROW.map(
({text, tooltip}) => ({text, tooltip}) =>
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`} <RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled} text={text} tooltip={tooltip} 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}
/>)}
{SECONDARY_SECOND_ROW.map( {SECONDARY_SECOND_ROW.map(
({text, tooltip}) => ({text, tooltip}) =>
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`} <RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled} text={text} tooltip={tooltip} 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}
/>)}
{SECONDARY_THIRD_ROW.map( {SECONDARY_THIRD_ROW.map(
({text, tooltip}) => ({text, tooltip}) =>
<RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`} <RSLocalButton key={`${prefixes.rsedit_btn}${tooltip}`}
text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled} text={text} tooltip={tooltip} onInsert={onEdit} disabled={disabled}
/>)} />)}
</div>
</div>
</div>); </div>);
} }

View File

@ -12,7 +12,7 @@ function RSLocalButton({ text, tooltip, disabled, onInsert }: RSLocalButtonProps
<button type='button' tabIndex={-1} <button type='button' tabIndex={-1}
disabled={disabled} disabled={disabled}
title={tooltip} 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)} onClick={() => onInsert(TokenID.ID_LOCAL, text)}
> >
{text} {text}

View File

@ -15,7 +15,7 @@ function RSTokenButton({ token, disabled, onInsert }: RSTokenButtonProps) {
disabled={disabled} disabled={disabled}
onClick={() => onInsert(token)} onClick={() => onInsert(token)}
title={describeToken(token)} title={describeToken(token)}
className={`px-1 cursor-pointer disabled:cursor-default border rounded-none h-6 ${width} outline-none clr-hover clr-btn-clear`} 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} {label ? <span className='whitespace-nowrap'>{label}</span> : null}
</button>); </button>);

View File

@ -2,22 +2,26 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiBug } from 'react-icons/bi';
import { ConceptLoader } from '@/components/Common/ConceptLoader';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import { ExpressionStatus } from '@/models/rsform'; import { ExpressionStatus } from '@/models/rsform';
import { type IConstituenta } from '@/models/rsform'; import { type IConstituenta } from '@/models/rsform';
import { inferStatus } from '@/models/rsformAPI'; import { inferStatus } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang'; import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { colorbgCstStatus } from '@/utils/color'; import { colorbgCstStatus } from '@/utils/color';
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels'; import { labelExpressionStatus } from '@/utils/labels';
interface StatusBarProps { interface StatusBarProps {
processing?: boolean
isModified?: boolean isModified?: boolean
parseData?: IExpressionParse parseData?: IExpressionParse
constituenta?: IConstituenta constituenta?: IConstituenta
onAnalyze: () => void
} }
function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) { function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze }: StatusBarProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const status = useMemo(() => { const status = useMemo(() => {
if (isModified) { if (isModified) {
@ -31,16 +35,28 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
}, [isModified, constituenta, parseData]); }, [isModified, constituenta, parseData]);
return ( return (
<div title={describeExpressionStatus(status)} <div
title='Проверить определение [Ctrl + Q]'
className={clsx( className={clsx(
'h-full', 'w-[10rem] h-[1.75rem]',
'border rounded-none', 'px-3',
'text-sm font-semibold small-caps text-center', 'border',
'select-none' 'select-none',
'cursor-pointer',
'duration-500 transition-colors'
)} )}
style={{backgroundColor: colorbgCstStatus(status, colors)}} style={{backgroundColor: processing ? colors.bgDefault : colorbgCstStatus(status, colors)}}
onClick={onAnalyze}
> >
{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)} {labelExpressionStatus(status)}
</span>
</div>
}
</div>); </div>);
} }

View File

@ -90,7 +90,7 @@ function FormRSForm({
/> />
<TextInput required <TextInput required
label='Сокращение' label='Сокращение'
dimensions='w-[14rem]' className='w-[14rem]'
pattern={patterns.alias} pattern={patterns.alias}
tooltip={`не более ${limits.alias_len} символов`} tooltip={`не более ${limits.alias_len} символов`}
disabled={disabled} disabled={disabled}
@ -107,7 +107,6 @@ function FormRSForm({
<Checkbox <Checkbox
label='Общедоступная схема' label='Общедоступная схема'
tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены' tooltip='Общедоступные схемы видны всем пользователям и могут быть изменены'
dimensions='w-fit'
disabled={disabled} disabled={disabled}
value={common} value={common}
setValue={value => setCommon(value)} setValue={value => setCommon(value)}
@ -115,7 +114,6 @@ function FormRSForm({
<Checkbox <Checkbox
label='Неизменная схема' label='Неизменная схема'
tooltip='Только администраторы могут присваивать схемам неизменный статус' tooltip='Только администраторы могут присваивать схемам неизменный статус'
dimensions='w-fit'
disabled={disabled || !user?.is_staff} disabled={disabled || !user?.is_staff}
value={canonical} value={canonical}
setValue={value => setCanonical(value)} setValue={value => setCanonical(value)}
@ -123,8 +121,7 @@ function FormRSForm({
</div> </div>
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center' className='self-center my-2'
dimensions='my-2 w-fit'
loading={processing} loading={processing}
disabled={!isModified || disabled} disabled={!isModified || disabled}
icon={<FiSave size='1.5rem' />} icon={<FiSave size='1.5rem' />}

View File

@ -128,7 +128,7 @@ function RSTable({
<DataTable dense noFooter <DataTable dense noFooter
className={clsx( className={clsx(
'min-h-[20rem]', 'min-h-[20rem]',
'overflow-auto', 'overflow-y-auto',
'text-sm', 'text-sm',
'select-none' 'select-none'
)} )}

View File

@ -111,7 +111,7 @@ function TermGraph({
}, [noNavigation]); }, [noNavigation]);
return ( return (
<div className='w-full h-full overflow-auto outline-none'> <div className='outline-none'>
<div className='relative' style={{width: canvasWidth, height: canvasHeight}}> <div className='relative' style={{width: canvasWidth, height: canvasHeight}}>
<GraphUI <GraphUI
draggable draggable

View File

@ -68,7 +68,7 @@ function RSTabs() {
cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate, resetAliases cstCreate, cstDelete, cstRename, subscribe, unsubscribe, cstUpdate, resetAliases
} = useRSForm(); } = useRSForm();
const { destroyItem } = useLibrary(); const { destroyItem } = useLibrary();
const { setNoFooter, noNavigation } = useConceptTheme(); const { setNoFooter } = useConceptTheme();
const { user } = useAuth(); const { user } = useAuth();
const { mode, setMode } = useAccessMode(); const { mode, setMode } = useAccessMode();
@ -106,13 +106,6 @@ function RSTabs() {
const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined); const [insertCstID, setInsertCstID] = useState<number | undefined>(undefined);
const [showTemplates, setShowTemplates] = useState(false); const [showTemplates, setShowTemplates] = useState(false);
const panelHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 4.8rem - 4px)'
: 'calc(100vh - 2rem - 4px)';
}, [noNavigation]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema) { if (schema) {
const oldTitle = document.title; const oldTitle = document.title;
@ -403,11 +396,11 @@ function RSTabs() {
onSelect={onSelectTab} onSelect={onSelectTab}
defaultFocus defaultFocus
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
className='flex flex-col items-center min-w-[45rem]' className='flex flex-col min-w-[45rem]'
> >
<TabList className={clsx( <TabList className={clsx(
'h-[1.9rem]', 'h-[1.9rem] mx-auto',
'flex justify-stretch', 'flex',
'border-b-2 border-x-2 divide-x-2' 'border-b-2 border-x-2 divide-x-2'
)}> )}>
<RSTabsMenu isMutable={isMutable} <RSTabsMenu isMutable={isMutable}
@ -435,7 +428,6 @@ function RSTabs() {
<ConceptTab label='Граф термов' /> <ConceptTab label='Граф термов' />
</TabList> </TabList>
<div className='overflow-y-auto' style={{ maxHeight: panelHeight}}>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
<EditorRSForm <EditorRSForm
isMutable={isMutable} isMutable={isMutable}
@ -481,7 +473,6 @@ function RSTabs() {
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
/> />
</TabPanel> </TabPanel>
</div>
</Tabs> : null} </Tabs> : null}
</>); </>);
} }

View File

@ -95,12 +95,12 @@ function RSTabsMenu({
} }
return ( return (
<div className='flex items-stretch h-full w-fit'> <div className='flex'>
<div ref={schemaMenu.ref}> <div ref={schemaMenu.ref}>
<Button noBorder dense tabIndex={-1} <Button noBorder dense tabIndex={-1}
tooltip='Меню' tooltip='Меню'
icon={<BiMenu size='1.25rem' className='clr-text-controls' />} icon={<BiMenu size='1.25rem' className='clr-text-controls' />}
dimensions='h-full w-fit pl-2' className='h-full pl-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
onClick={schemaMenu.toggle} onClick={schemaMenu.toggle}
/> />
@ -148,7 +148,7 @@ function RSTabsMenu({
<div ref={editMenu.ref}> <div ref={editMenu.ref}>
<Button dense noBorder tabIndex={-1} <Button dense noBorder tabIndex={-1}
tooltip={'Редактирование'} tooltip={'Редактирование'}
dimensions='h-full w-fit' className='h-full'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>} icon={<FiEdit size='1.25rem' className={isMutable ? 'clr-text-success' : 'clr-text-warning'}/>}
onClick={editMenu.toggle} onClick={editMenu.toggle}
@ -173,7 +173,7 @@ function RSTabsMenu({
<div ref={accessMenu.ref}> <div ref={accessMenu.ref}>
<Button dense noBorder tabIndex={-1} <Button dense noBorder tabIndex={-1}
tooltip={`режим ${labelAccessMode(mode)}`} tooltip={`режим ${labelAccessMode(mode)}`}
dimensions='h-full w-fit pr-2' className='h-full pr-2'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
icon={ icon={
mode === UserAccessMode.ADMIN ? <BiMeteor size='1.25rem' className='clr-text-primary'/> mode === UserAccessMode.ADMIN ? <BiMeteor size='1.25rem' className='clr-text-primary'/>

View File

@ -93,18 +93,18 @@ function RegisterPage() {
pattern={patterns.login} pattern={patterns.login}
tooltip='Минимум 3 знака. Латинские буквы и цифры. Не может начинаться с цифры' tooltip='Минимум 3 знака. Латинские буквы и цифры. Не может начинаться с цифры'
value={username} value={username}
dimensions='w-[15rem]' className='w-[15rem]'
onChange={event => setUsername(event.target.value)} onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='password' type='password' required <TextInput id='password' type='password' required
label='Пароль' label='Пароль'
dimensions='w-[15rem]' className='w-[15rem]'
value={password} value={password}
onChange={event => setPassword(event.target.value)} onChange={event => setPassword(event.target.value)}
/> />
<TextInput id='password2' required type='password' <TextInput id='password2' required type='password'
label='Повторите пароль' label='Повторите пароль'
dimensions='w-[15rem]' className='w-[15rem]'
value={password2} value={password2}
onChange={event => setPassword2(event.target.value)} onChange={event => setPassword2(event.target.value)}
/> />
@ -145,13 +145,13 @@ function RegisterPage() {
<div className='flex justify-around my-3'> <div className='flex justify-around my-3'>
<SubmitButton <SubmitButton
text='Регистрировать' text='Регистрировать'
dimensions='min-w-[10rem]' className='min-w-[10rem]'
loading={loading} loading={loading}
disabled={!acceptPrivacy} disabled={!acceptPrivacy}
/> />
<Button <Button
text='Назад' text='Назад'
dimensions='min-w-[10rem]' className='min-w-[10rem]'
onClick={() => handleCancel()} onClick={() => handleCancel()}
/> />
</div> </div>

View File

@ -53,7 +53,7 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
return ( return (
<DataTable dense noFooter <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} columns={columns}
data={items} data={items}
headPosition='0' headPosition='0'

View File

@ -650,6 +650,8 @@ export function describeRSError(error: IRSErrorDescription): string {
return `Непустое выражение базисного/константного множества`; return `Непустое выражение базисного/константного множества`;
case RSErrorType.globalUnexpectedType: case RSErrorType.globalUnexpectedType:
return `Типизация выражения не соответствует типу конституенты`; return `Типизация выражения не соответствует типу конституенты`;
case RSErrorType.globalEmptyDerived:
return `Пустое выражение для выводимого понятия или утверждения`;
} }
return 'UNKNOWN ERROR'; return 'UNKNOWN ERROR';
} }