Implementing templated expressions pt1

This commit is contained in:
IRBorisov 2023-11-04 01:29:21 +03:00
parent 90c8303ee7
commit 6e610b5806
13 changed files with 396 additions and 89 deletions

View File

@ -9,15 +9,17 @@ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'>
colorClass?: string colorClass?: string
dense?: boolean dense?: boolean
noBorder?: boolean noBorder?: boolean
noOutline?: boolean
} }
function TextInput({ function TextInput({
id, required, label, dense, tooltip, noBorder, id, required, label, dense, tooltip, noBorder, noOutline,
dimensions = 'w-full', dimensions = 'w-full',
colorClass = 'clr-input', colorClass = 'clr-input',
...props ...props
}: TextInputProps) { }: TextInputProps) {
const borderClass = noBorder ? '': 'border'; const borderClass = noBorder ? '': 'border';
const outlineClass = noOutline ? '': 'clr-outline';
return ( return (
<div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}> <div className={`flex ${dense ? 'items-center gap-4 ' + dimensions : 'flex-col items-start gap-2'}`}>
{label && {label &&
@ -27,7 +29,7 @@ function TextInput({
/>} />}
<input id={id} <input id={id}
title={tooltip} title={tooltip}
className={`px-3 py-2 leading-tight ${borderClass} truncate hover:text-clip clr-outline ${colorClass} ${dense ? 'w-full' : dimensions}`} className={`px-3 py-2 leading-tight truncate hover:text-clip ${outlineClass} ${borderClass} ${colorClass} ${dense ? 'w-full' : dimensions}`}
required={required} required={required}
{...props} {...props}
/> />

View File

@ -6,7 +6,7 @@ function HelpConstituenta() {
<div> <div>
<h1>Подсказки</h1> <h1>Подсказки</h1>
<p><b className='text-warning'>Изменения сохраняются ПОСЛЕ нажатия на соответствующую кнопку снизу или по центру</b></p> <p><b className='text-warning'>Изменения сохраняются ПОСЛЕ нажатия на соответствующую кнопку снизу или по центру</b></p>
<p><b>Клик на формальное выражение</b> - обратите внимание на кнопки снизу<br/>Горячие клавиши указаны в подсказках при наведении</p> <p><b>Формальное определение</b> - обратите внимание на кнопки снизу<br/>Горячие клавиши указаны в подсказках при наведении</p>
<p><b>Поля Термин и Определение</b> - Ctrl + Пробел открывает диалог редактирования отсылок<br/>Перед открытием диалога переместите текстовый курсор на заменяемое слово или ссылку</p> <p><b>Поля Термин и Определение</b> - Ctrl + Пробел открывает диалог редактирования отсылок<br/>Перед открытием диалога переместите текстовый курсор на заменяемое слово или ссылку</p>
<p><b>Список конституент справа</b> - обратите внимание на настройки фильтрации</p> <p><b>Список конституент справа</b> - обратите внимание на настройки фильтрации</p>
<p>- слева от ввода текста настраивается набор атрибутов конституенты</p> <p>- слева от ввода текста настраивается набор атрибутов конституенты</p>

View File

@ -4,8 +4,8 @@ import { ErrorInfo } from '../components/BackendError';
import { matchLibraryItem } from '../models/library'; import { matchLibraryItem } from '../models/library';
import { ILibraryItem } from '../models/library'; import { ILibraryItem } from '../models/library';
import { ILibraryFilter } from '../models/miscelanious'; import { ILibraryFilter } from '../models/miscelanious';
import { IRSFormCreateData, IRSFormData } from '../models/rsform'; import { IRSForm, IRSFormCreateData, IRSFormData, loadRSFormData } from '../models/rsform';
import { DataCallback, deleteLibraryItem, getLibrary, getTemplates, postCloneLibraryItem, postNewRSForm } from '../utils/backendAPI'; import { DataCallback, deleteLibraryItem, getLibrary, getRSFormDetails, getTemplates, postCloneLibraryItem, postNewRSForm } from '../utils/backendAPI';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
interface ILibraryContext { interface ILibraryContext {
@ -17,6 +17,7 @@ interface ILibraryContext {
setError: (error: ErrorInfo) => void setError: (error: ErrorInfo) => void
applyFilter: (params: ILibraryFilter) => ILibraryItem[] applyFilter: (params: ILibraryFilter) => ILibraryItem[]
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void
destroyItem: (target: number, callback?: () => void) => void destroyItem: (target: number, callback?: () => void) => void
@ -38,13 +39,15 @@ interface LibraryStateProps {
} }
export const LibraryState = ({ children }: LibraryStateProps) => { export const LibraryState = ({ children }: LibraryStateProps) => {
const [ items, setItems ] = useState<ILibraryItem[]>([]) const [ items, setItems ] = useState<ILibraryItem[]>([]);
const [ templates, setTemplates ] = useState<ILibraryItem[]>([]) const [ templates, setTemplates ] = useState<ILibraryItem[]>([]);
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const [ processing, setProcessing ] = useState(false); const [ processing, setProcessing ] = useState(false);
const [ error, setError ] = useState<ErrorInfo>(undefined); const [ error, setError ] = useState<ErrorInfo>(undefined);
const { user } = useAuth(); const { user } = useAuth();
const [ cachedTemplates, setCachedTemplates ] = useState<IRSForm[]>([]);
const applyFilter = useCallback( const applyFilter = useCallback(
(params: ILibraryFilter) => { (params: ILibraryFilter) => {
let result = items; let result = items;
@ -69,6 +72,26 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
return result; return result;
}, [items, user]); }, [items, user]);
const retrieveTemplate = useCallback(
(templateID: number, callback: (schema: IRSForm) => void) => {
const cached = cachedTemplates.find(schema => schema.id == templateID);
if (cached) {
callback(cached);
return;
}
setError(undefined);
getRSFormDetails(String(templateID), {
showError: true,
setLoading: setLoading,
onError: error => setError(error),
onSuccess: data => {
const schema = loadRSFormData(data);
setCachedTemplates(prev => ([...prev, schema]));
callback(schema);
}
});
}, [cachedTemplates]);
const reload = useCallback( const reload = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
setItems([]); setItems([]);
@ -150,7 +173,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
return ( return (
<LibraryContext.Provider value={{ <LibraryContext.Provider value={{
items, templates, loading, processing, error, setError, items, templates, loading, processing, error, setError,
applyFilter, createItem, cloneItem, destroyItem applyFilter, createItem, cloneItem, destroyItem, retrieveTemplate
}}> }}>
{ children } { children }
</LibraryContext.Provider> </LibraryContext.Provider>

View File

@ -77,7 +77,7 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
spellCheck spellCheck
onChange={event => updateCstData({ term_raw: event.target.value })} onChange={event => updateCstData({ term_raw: event.target.value })}
/> />
<RSInput id='expression' label='Формальное выражение' <RSInput id='expression' label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
editable editable
height='4.8rem' height='4.8rem'

View File

@ -179,7 +179,6 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
</div>); </div>);
}, [handleSelectGrams, selectedGrams]); }, [handleSelectGrams, selectedGrams]);
const columnsConstituenta = useMemo( const columnsConstituenta = useMemo(
() => [ () => [
constituentaHelper.accessor('alias', { constituentaHelper.accessor('alias', {

View File

@ -0,0 +1,65 @@
import { Dispatch } from 'react';
import SelectSingle from '../../components/Common/SelectSingle';
import TextArea from '../../components/Common/TextArea';
import TextInput from '../../components/Common/TextInput';
import RSInput from '../../components/RSInput';
import { CstType, ICstCreateData } from '../../models/rsform';
import { labelCstType } from '../../utils/labels';
import { SelectorCstType } from '../../utils/selectors';
interface ConstituentaTabProps {
state: ICstCreateData
partialUpdate: Dispatch<Partial<ICstCreateData>>
}
function ConstituentaTab({state, partialUpdate}: ConstituentaTabProps) {
return (
<div className='flex flex-col gap-2'>
<div className='flex justify-center w-full gap-6'>
<SelectSingle
className='my-2 min-w-[15rem] self-center'
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]'
value={state.alias}
onChange={event => partialUpdate({ alias: event.target.value})}
/>
</div>
<TextArea id='term' label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2}
value={state.term_raw}
spellCheck
onChange={event => partialUpdate({ term_raw: event.target.value })}
/>
<RSInput id='expression' label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение'
editable
height='4.8rem'
value={state.definition_formal}
onChange={value => partialUpdate({definition_formal: value})}
/>
<TextArea id='definition' label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
rows={2}
value={state.definition_raw}
spellCheck
onChange={event => partialUpdate({ definition_raw: event.target.value })}
/>
<TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={2}
value={state.convention}
spellCheck
onChange={event => partialUpdate({ convention: event.target.value })}
/>
</div>);
}
export default ConstituentaTab;

View File

@ -0,0 +1,222 @@
import { Dispatch, useEffect, useMemo, useState } from 'react';
import SelectSingle from '../../components/Common/SelectSingle';
import TextArea from '../../components/Common/TextArea';
import TextInput from '../../components/Common/TextInput';
import DataTable, { createColumnHelper,IConditionalStyle } from '../../components/DataTable';
import ConstituentaTooltip from '../../components/Help/ConstituentaTooltip';
import { MagnifyingGlassIcon } from '../../components/Icons';
import RSInput from '../../components/RSInput';
import { useLibrary } from '../../context/LibraryContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { CstMatchMode } from '../../models/miscelanious';
import { applyFilterCategory, CATEGORY_CST_TYPE, IConstituenta, IRSForm, matchConstituenta } from '../../models/rsform';
import { colorfgCstStatus } from '../../utils/color';
import { prefixes } from '../../utils/constants';
export interface ITemplateState {
templateID?: number
prototype?: IConstituenta
filterCategory?: IConstituenta
filterText: string
}
interface TemplateTabProps {
state: ITemplateState
partialUpdate: Dispatch<Partial<ITemplateState>>
}
const constituentaHelper = createColumnHelper<IConstituenta>();
function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
const { colors } = useConceptTheme();
const { templates, retrieveTemplate } = useLibrary();
const [ selectedSchema, setSelectedSchema ] = useState<IRSForm | undefined>(undefined);
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const prototypeInfo = useMemo(
() => {
if (!state.prototype) {
return '';
} else {
return `${state.prototype?.term_raw}${state.prototype?.definition_raw ? `${state.prototype?.definition_raw}` : ''}`;
}
}, [state.prototype]);
const templateSelector = useMemo(
() => templates.map(
(template) => ({
value: template.id,
label: template.title
})
), [templates]);
const categorySelector = useMemo(
(): {value: number, label: string}[] => {
if (!selectedSchema) {
return [];
}
return selectedSchema.items
.filter(cst => cst.cst_type === CATEGORY_CST_TYPE)
.map(cst => ({
value: cst.id,
label: cst.term_raw
}));
}, [selectedSchema]);
useEffect(
() => {
if (templates.length > 0 && !state.templateID) {
partialUpdate({ templateID: templates[0].id });
}
}, [templates, state.templateID, partialUpdate]);
useEffect(
() => {
if (!state.templateID) {
setSelectedSchema(undefined);
} else {
retrieveTemplate(state.templateID, setSelectedSchema);
}
}, [state.templateID, retrieveTemplate]);
// Filter constituents
useEffect(
() => {
if (!selectedSchema) {
return;
}
let data = selectedSchema.items;
if (state.filterCategory) {
data = applyFilterCategory(state.filterCategory, selectedSchema);
}
if (state.filterText) {
data = data.filter(cst => matchConstituenta(state.filterText, cst, CstMatchMode.TERM));
}
setFilteredData(data);
}, [state.filterText, state.filterCategory, selectedSchema]);
function handleSelectTemplate(cst: IConstituenta) {
partialUpdate( { prototype: cst } );
}
const columns = useMemo(
() => [
constituentaHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
cell: props => {
const cst = props.row.original;
return (<>
<div
id={`${prefixes.cst_template_ist}${cst.alias}`}
className='min-w-[3.1rem] max-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap'
style={{
borderWidth: '1px',
borderColor: colorfgCstStatus(cst.status, colors),
color: colorfgCstStatus(cst.status, colors),
fontWeight: 600
}}
>
{cst.alias}
</div>
<ConstituentaTooltip data={cst} anchor={`#${prefixes.cst_template_ist}${cst.alias}`} />
</>);
}
}),
constituentaHelper.accessor('term_resolved', {
id: 'term',
header: 'Термин',
size: 600,
minSize: 350,
maxSize: 600
})
], [colors]);
const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [
{
when: (cst: IConstituenta) => cst.id === state.prototype?.id,
style: {
backgroundColor: colors.bgSelected
},
}
], [state.prototype, colors]);
return (
<div className='flex flex-col gap-2 mt-2'>
<div className='flex justify-between gap-4'>
<SelectSingle
className='w-full'
options={categorySelector}
placeholder='Выберите категорию'
value={state.filterCategory && selectedSchema ? {
value: state.filterCategory.id,
label: state.filterCategory.term_raw
} : null}
onChange={data => partialUpdate({filterCategory: selectedSchema?.items.find(cst => cst.id === data?.value) })}
isClearable
/>
<SelectSingle
className='min-w-[15rem]'
options={templateSelector}
placeholder='Выберите источник'
value={state.templateID ? { value: state.templateID, label: templates.find(item => item.id == state.templateID)!.title }: null}
onChange={data => partialUpdate({templateID: (data ? data.value : undefined)})}
/>
</div>
<div>
<div className='relative'>
<div className='absolute inset-y-0 flex items-center pl-3 pointer-events-none text-controls'>
<MagnifyingGlassIcon />
</div>
<TextInput
dimensions='w-full pl-10'
placeholder='Поиск'
noOutline
value={state.filterText}
onChange={event => partialUpdate({ filterText: event.target.value} )}
/>
</div>
<div className='border min-h-[15.5rem] max-h-[15.5rem] text-sm overflow-y-auto select-none'>
<DataTable
data={filteredData}
columns={columns}
conditionalRowStyles={conditionalRowStyles}
headPosition='0'
dense
noDataComponent={
<span className='flex flex-col justify-center p-2 text-center min-h-[5rem]'>
<p>Список конституент пуст</p>
<p>Измените параметры фильтра</p>
</span>
}
onRowClicked={handleSelectTemplate}
/>
</div>
</div>
<TextArea id='term'
rows={2}
disabled
placeholder='Шаблон конституенты не выбран'
value={prototypeInfo}
spellCheck
/>
<RSInput id='expression'
height='4.8rem'
placeholder='Выберите шаблон из списка'
editable={false}
value={state.prototype?.definition_formal}
/>
</div>);
}
export default TemplateTab;

View File

@ -2,18 +2,14 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import ConceptTooltip from '../../components/Common/ConceptTooltip'; import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Modal, { ModalProps } from '../../components/Common/Modal'; import Modal, { ModalProps } from '../../components/Common/Modal';
import SelectSingle from '../../components/Common/SelectSingle';
import SwitchButton from '../../components/Common/SwitchButton'; import SwitchButton from '../../components/Common/SwitchButton';
import TextArea from '../../components/Common/TextArea';
import TextInput from '../../components/Common/TextInput';
import HelpRSTemplates from '../../components/Help/HelpRSTemplates'; import HelpRSTemplates from '../../components/Help/HelpRSTemplates';
import { HelpIcon } from '../../components/Icons'; import { HelpIcon } from '../../components/Icons';
import RSInput from '../../components/RSInput';
import usePartialUpdate from '../../hooks/usePartialUpdate'; import usePartialUpdate from '../../hooks/usePartialUpdate';
import { CstType, ICstCreateData, IRSForm } from '../../models/rsform'; import { CstType, ICstCreateData, IRSForm } from '../../models/rsform';
import { labelCstType } from '../../utils/labels';
import { createAliasFor, validateCstAlias } from '../../utils/misc'; import { createAliasFor, validateCstAlias } from '../../utils/misc';
import { SelectorCstType } from '../../utils/selectors'; import ConstituentaTab from './ConstituentaTab';
import TemplateTab, { ITemplateState } from './TemplateTab';
interface DlgTemplatesProps interface DlgTemplatesProps
extends Pick<ModalProps, 'hideWindow'> { extends Pick<ModalProps, 'hideWindow'> {
@ -23,7 +19,7 @@ extends Pick<ModalProps, 'hideWindow'> {
function DlgTemplates({ hideWindow, schema, onCreate }: DlgTemplatesProps) { function DlgTemplates({ hideWindow, schema, onCreate }: DlgTemplatesProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [cstData, updateCstData] = usePartialUpdate({ const [cstData, updateCstData] = usePartialUpdate<ICstCreateData>({
cst_type: CstType.TERM, cst_type: CstType.TERM,
insert_after: null, insert_after: null,
alias: '', alias: '',
@ -33,6 +29,9 @@ function DlgTemplates({ hideWindow, schema, onCreate }: DlgTemplatesProps) {
term_raw: '', term_raw: '',
term_forms: [] term_forms: []
}); });
const [ templateData, updateTemplateData ] = usePartialUpdate<ITemplateState>({
filterText: ''
});
const [ showAttributes, setShowAttributes ] = useState(false); const [ showAttributes, setShowAttributes ] = useState(false);
@ -48,6 +47,24 @@ function DlgTemplates({ hideWindow, schema, onCreate }: DlgTemplatesProps) {
setValidated(validateCstAlias(cstData.alias, cstData.cst_type, schema)); setValidated(validateCstAlias(cstData.alias, cstData.cst_type, schema));
}, [cstData.alias, cstData.cst_type, schema]); }, [cstData.alias, cstData.cst_type, schema]);
useLayoutEffect(
() => {
if (!templateData.prototype) {
updateCstData({
definition_raw: '',
definition_formal: '',
term_raw: ''
});
} else {
updateCstData({
cst_type: templateData.prototype.cst_type,
definition_raw: templateData.prototype.definition_raw,
definition_formal: templateData.prototype.definition_formal,
term_raw: templateData.prototype.term_raw
});
}
}, [templateData.prototype, updateCstData]);
return ( return (
<Modal <Modal
title='Создание конституенты из шаблона' title='Создание конституенты из шаблона'
@ -87,56 +104,16 @@ function DlgTemplates({ hideWindow, schema, onCreate }: DlgTemplatesProps) {
</div> </div>
{ !showAttributes && { !showAttributes &&
<div> <TemplateTab
Выбор шаблона и параметров state={templateData}
</div>} partialUpdate={updateTemplateData}
/>}
{ showAttributes && { showAttributes &&
<div> <ConstituentaTab
<div className='flex justify-center w-full gap-6'> state={cstData}
<SelectSingle partialUpdate={updateCstData}
className='my-2 min-w-[15rem] self-center' />}
options={SelectorCstType}
placeholder='Выберите тип'
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }}
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.TERM})}
/>
<TextInput id='alias' label='Имя'
dense
dimensions='w-[7rem]'
value={cstData.alias}
onChange={event => updateCstData({ alias: event.target.value})}
/>
</div>
<TextArea id='term' label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2}
value={cstData.term_raw}
spellCheck
onChange={event => updateCstData({ term_raw: event.target.value })}
/>
<RSInput id='expression' label='Формальное выражение'
placeholder='Родоструктурное выражение, задающее формальное определение'
editable
height='4.8rem'
value={cstData.definition_formal}
onChange={value => updateCstData({definition_formal: value})}
/>
<TextArea id='definition' label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
rows={2}
value={cstData.definition_raw}
spellCheck
onChange={event => updateCstData({ definition_raw: event.target.value })}
/>
<TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={2}
value={cstData.convention}
spellCheck
onChange={event => updateCstData({ convention: event.target.value })}
/>
</div>}
</div> </div>
</Modal>); </Modal>);
} }

View File

@ -16,6 +16,9 @@ export enum CstType {
THEOREM = 'theorem' THEOREM = 'theorem'
} }
// CstType constant for category dividers in TemplateSchemas. TODO: create separate sctructure for templates
export const CATEGORY_CST_TYPE = CstType.THEOREM;
export enum CstClass { export enum CstClass {
BASIC = 'basic', BASIC = 'basic',
DERIVED = 'derived', DERIVED = 'derived',
@ -307,3 +310,18 @@ export function createMockConstituenta(schema: number, id: number, alias: string
}; };
} }
export function applyFilterCategory(target: IConstituenta, schema: IRSFormData): IConstituenta[] {
const nextCategory = schema.items.find(
cst => (
cst.order > target.order &&
cst.cst_type === CATEGORY_CST_TYPE
)
);
return schema.items.filter(
cst => (
cst.order > target.order &&
(!nextCategory || cst.order <= nextCategory.order)
)
);
}

View File

@ -229,7 +229,7 @@ function EditorConstituenta({
colorClass='clr-app' colorClass='clr-app'
disabled disabled
/> />
<EditorRSExpression id='expression' label='Формальное выражение' <EditorRSExpression id='expression' label='Формальное определение'
activeCst={activeCst} activeCst={activeCst}
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression} value={expression}

View File

@ -118,7 +118,7 @@ function EditorRSExpression({
return ( return (
<div className='flex flex-col items-start w-full'> <div className='flex flex-col items-start w-full'>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-[-0.2rem] left-[10.5rem]'> <div className='absolute top-[-0.2rem] left-[11rem]'>
<MiniButton <MiniButton
tooltip='Дерево разбора выражения' tooltip='Дерево разбора выражения'
noHover noHover
@ -141,7 +141,7 @@ function EditorRSExpression({
<div className='w-full max-h-[5rem] min-h-[5rem] flex'> <div className='w-full max-h-[5rem] min-h-[5rem] flex'>
<div className='flex flex-col'> <div className='flex flex-col'>
<Button <Button
tooltip='Проверить формальное выражение' tooltip='Проверить формальное определение'
text='Проверить' text='Проверить'
dimensions='w-fit h-[3rem] z-pop' dimensions='w-fit h-[3rem] z-pop'
colorClass='clr-btn-default' colorClass='clr-btn-default'

View File

@ -34,6 +34,7 @@ export const globalIDs = {
export const prefixes = { export const prefixes = {
cst_list: 'cst-list-', cst_list: 'cst-list-',
cst_template_ist: 'cst-template-list-',
cst_wordform_list: 'cst-wordform-list-', cst_wordform_list: 'cst-wordform-list-',
cst_status_list: 'cst-status-list-', cst_status_list: 'cst-status-list-',
cst_match_mode_list: 'cst-match-mode-list-', cst_match_mode_list: 'cst-match-mode-list-',