Reimplement CstCreate and minor UI fixes

This commit is contained in:
IRBorisov 2023-08-16 18:32:37 +03:00
parent 1f9e761565
commit 4fbfda4eb4
21 changed files with 282 additions and 156 deletions

View File

@ -69,11 +69,20 @@ class StandaloneCstSerializer(serializers.ModelSerializer):
return attrs return attrs
class CstCreateSerializer(serializers.Serializer): class CstCreateSerializer(serializers.ModelSerializer):
alias = serializers.CharField(max_length=8)
cst_type = serializers.CharField(max_length=10)
insert_after = serializers.IntegerField(required=False, allow_null=True) insert_after = serializers.IntegerField(required=False, allow_null=True)
class Meta:
model = Constituenta
fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after'
def validate(self, attrs):
if ('term_raw' in attrs):
attrs['term_resolved'] = attrs['term_raw']
if ('definition_raw' in attrs):
attrs['definition_resolved'] = attrs['definition_raw']
return attrs
class CstListSerlializer(serializers.Serializer): class CstListSerlializer(serializers.Serializer):
items = serializers.ListField( items = serializers.ListField(

View File

@ -215,6 +215,28 @@ class TestRSFormViewset(APITestCase):
x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias']) x4 = Constituenta.objects.get(alias=response.data['new_cst']['alias'])
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
def test_create_constituenta_data(self):
data = json.dumps({
'alias': 'X3',
'cst_type': 'basic',
'convention': '1',
'term_raw': '2',
'definition_formal': '3',
'definition_raw': '4'
})
schema = self.rsform_owned
response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['new_cst']['alias'], 'X3')
self.assertEqual(response.data['new_cst']['cst_type'], 'basic')
self.assertEqual(response.data['new_cst']['convention'], '1')
self.assertEqual(response.data['new_cst']['term_raw'], '2')
self.assertEqual(response.data['new_cst']['term_resolved'], '2')
self.assertEqual(response.data['new_cst']['definition_formal'], '3')
self.assertEqual(response.data['new_cst']['definition_raw'], '4')
self.assertEqual(response.data['new_cst']['definition_resolved'], '4')
def test_delete_constituenta(self): def test_delete_constituenta(self):
schema = self.rsform_owned schema = self.rsform_owned
data = json.dumps({'items': [{'id': 1337}]}) data = json.dumps({'items': [{'id': 1337}]})

View File

@ -82,6 +82,14 @@ class RSFormViewSet(viewsets.ModelViewSet):
serializer.validated_data['cst_type']) serializer.validated_data['cst_type'])
else: else:
constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['cst_type']) constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['cst_type'])
constituenta.convention = serializer.validated_data.get('convention', '')
constituenta.term_raw = serializer.validated_data.get('term_raw', '')
constituenta.term_resolved = serializer.validated_data.get('term_resolved', '')
constituenta.definition_formal = serializer.validated_data.get('definition_formal', '')
constituenta.definition_raw = serializer.validated_data.get('definition_raw', '')
constituenta.definition_resolved = serializer.validated_data.get('definition_resolved', '')
constituenta.save()
schema.refresh_from_db() schema.refresh_from_db()
outSerializer = serializers.RSFormDetailsSerlializer(schema) outSerializer = serializers.RSFormDetailsSerlializer(schema)
response = Response(status=201, data={ response = Response(status=201, data={

View File

@ -8,16 +8,17 @@ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
label: string label: string
widthClass?: string widthClass?: string
colorClass?: string colorClass?: string
singleRow?: boolean
} }
function TextInput({ function TextInput({
id, required, label, id, required, label, singleRow,
widthClass = 'w-full', widthClass = 'w-full',
colorClass = 'clr-input', colorClass = 'clr-input',
...props ...props
}: TextInputProps) { }: TextInputProps) {
return ( return (
<div className='flex flex-col items-start [&:not(:first-child)]:mt-3'> <div className={`flex ${singleRow ? 'items-center gap-4' : 'flex-col items-start'} [&:not(:first-child)]:mt-3`}>
<Label <Label
text={label} text={label}
required={required} required={required}

View File

@ -7,7 +7,7 @@ interface TextURLProps {
function TextURL({ text, href }: TextURLProps) { function TextURL({ text, href }: TextURLProps) {
return ( return (
<Link className='text-sm font-bold text-blue-400 dark:text-orange-600 dark:hover:text-orange-400 hover:underline hover:text-blue-600' to={href}> <Link className='font-bold hover:underline clr-url' to={href}>
{text} {text}
</Link> </Link>
); );

View File

@ -1,22 +1,17 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import ConceptTooltip from '../Common/ConceptTooltip'; import ConceptTooltip from '../Common/ConceptTooltip';
import TextURL from '../Common/TextURL'; import TextURL from '../Common/TextURL';
import { BellIcon, PlusIcon, SquaresIcon } from '../Icons'; import { PlusIcon, SquaresIcon } from '../Icons';
import NavigationButton from './NavigationButton'; import NavigationButton from './NavigationButton';
function UserTools() { function UserTools() {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const navigateCreateRSForm = () => { navigate('/rsform-create'); }; const navigateCreateRSForm = () => navigate('/rsform-create');
const navigateMyWork = () => { navigate('/library?filter=personal'); }; const navigateMyWork = () => navigate('/library?filter=personal');
const handleNotifications = () => {
toast.info('Уведомления в разработке');
};
return ( return (
<div className='flex items-center px-2 border-r-2 border-gray-400 dark:border-gray-300'> <div className='flex items-center px-2 border-r-2 border-gray-400 dark:border-gray-300'>
@ -30,7 +25,6 @@ function UserTools() {
/>} />}
{ !user && { !user &&
<NavigationButton id='items-nav-help' <NavigationButton id='items-nav-help'
description='Невозможно создать новую схему. Войдите в систему'
icon={<PlusIcon />} icon={<PlusIcon />}
/>} />}
<ConceptTooltip anchorSelect='#items-nav-help' clickable> <ConceptTooltip anchorSelect='#items-nav-help' clickable>
@ -45,7 +39,6 @@ function UserTools() {
</ConceptTooltip> </ConceptTooltip>
</span> </span>
{ user && <NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} /> } { user && <NavigationButton icon={<SquaresIcon />} description='Мои схемы' onClick={navigateMyWork} /> }
{ user && user.is_staff && <NavigationButton icon={<BellIcon />} description='Уведомления' onClick={handleNotifications} />}
</div> </div>
); );
} }

View File

@ -4,12 +4,15 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes'; import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import { Ref, useMemo } from 'react'; import { RefObject, useCallback, useMemo, useRef } from 'react';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { TokenID } from '../../utils/enums';
import Label from '../Common/Label';
import { ccBracketMatching } from './bracketMatching'; import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang'; import { RSLanguage } from './rslang';
import { getSymbolSubstitute,TextWrapper } from './textEditing';
import { rshoverTooltip } from './tooltip'; import { rshoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
@ -41,19 +44,25 @@ const editorSetup: BasicSetupOptions = {
}; };
interface RSInputProps interface RSInputProps
extends Omit<ReactCodeMirrorProps, 'onChange'> { extends Omit<ReactCodeMirrorProps, 'onChange'| 'onKeyDown'> {
innerref?: Ref<ReactCodeMirrorRef> | undefined label?: string
onChange: (newValue: string) => void innerref?: RefObject<ReactCodeMirrorRef> | undefined
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void onChange?: (newValue: string) => void
} }
function RSInput({ function RSInput({
innerref, onChange, editable, id, label, innerref, onChange, editable,
...props ...props
}: RSInputProps) { }: RSInputProps) {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
const { schema } = useRSForm(); const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(
() => {
return innerref ?? internalRef;
}, [internalRef, innerref])
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]); const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
const lightTheme: Extension = useMemo( const lightTheme: Extension = useMemo(
() => createTheme({ () => createTheme({
@ -105,16 +114,47 @@ function RSInput({
rshoverTooltip(schema?.items || []), rshoverTooltip(schema?.items || []),
], [darkMode, schema?.items]); ], [darkMode, schema?.items]);
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current) {
return;
}
const text = new TextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
if (event.shiftKey && event.key === '*' && !event.altKey) {
text.insertToken(TokenID.DECART);
} else if (event.altKey) {
if (!text.processAltKey(event.key)) {
return;
}
} else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.key);
if (!newSymbol) {
return;
}
text.replaceWith(newSymbol);
} else {
return;
}
event.preventDefault();
}, [thisRef]);
return ( return (
<div className={`w-full ${cursor} text-lg`}> <div className={`w-full ${cursor} text-lg`}>
<CodeMirror {label &&
ref={innerref} <Label
text={label}
required={false}
htmlFor={id}
/>}
<CodeMirror id={id}
ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}
theme={darkMode ? darkTheme : lightTheme} theme={darkMode ? darkTheme : lightTheme}
extensions={editorExtensions} extensions={editorExtensions}
indentWithTab={false} indentWithTab={false}
onChange={value => onChange(value)} onChange={onChange}
editable={editable} editable={editable}
onKeyDown={handleInput}
{...props} {...props}
/> />
</div> </div>

View File

@ -3,7 +3,7 @@ import { createContext, useCallback, useContext, useState } from 'react';
interface INavSearchContext { interface INavSearchContext {
query: string query: string
setQuery: (value: string) => void setQuery: (value: string) => void
cleanQuery: () => void resetQuery: () => void
} }
const NavSearchContext = createContext<INavSearchContext | null>(null); const NavSearchContext = createContext<INavSearchContext | null>(null);
@ -24,13 +24,13 @@ interface NavSearchStateProps {
export const NavSearchState = ({ children }: NavSearchStateProps) => { export const NavSearchState = ({ children }: NavSearchStateProps) => {
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const cleanQuery = useCallback(() => setQuery(''), []); const resetQuery = useCallback(() => setQuery(''), []);
return ( return (
<NavSearchContext.Provider value={{ <NavSearchContext.Provider value={{
query, query,
setQuery, setQuery,
cleanQuery resetQuery: resetQuery
}}> }}>
{children} {children}
</NavSearchContext.Provider> </NavSearchContext.Provider>

View File

@ -102,6 +102,10 @@
@apply bg-white dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500 @apply bg-white dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500
} }
.clr-url {
@apply hover:text-blue-600 text-blue-400 dark:text-orange-600 dark:hover:text-orange-400
}
.text-red { .text-red {
@apply text-red-600 dark:text-red-400 @apply text-red-600 dark:text-red-400
} }

View File

@ -58,7 +58,7 @@ function CreateRSFormPage() {
<RequireAuth> <RequireAuth>
<Form title='Создание концептуальной схемы' <Form title='Создание концептуальной схемы'
onSubmit={handleSubmit} onSubmit={handleSubmit}
widthClass='max-w-lg mt-4' widthClass='max-w-lg w-full mt-4'
> >
<TextInput id='title' label='Полное название' type='text' <TextInput id='title' label='Полное название' type='text'
required={!file} required={!file}
@ -67,10 +67,10 @@ function CreateRSFormPage() {
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
/> />
<TextInput id='alias' label='Сокращение' type='text' <TextInput id='alias' label='Сокращение' type='text'
singleRow
required={!file} required={!file}
value={alias} value={alias}
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
widthClass='max-w-sm'
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextArea id='comment' label='Комментарий' <TextArea id='comment' label='Комментарий'

View File

@ -3,6 +3,8 @@ import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import ConceptDataTable from '../../components/Common/ConceptDataTable'; import ConceptDataTable from '../../components/Common/ConceptDataTable';
import TextURL from '../../components/Common/TextURL';
import { useNavSearch } from '../../context/NavSearchContext';
import { useUsers } from '../../context/UsersContext'; import { useUsers } from '../../context/UsersContext';
import { IRSFormMeta } from '../../utils/models' import { IRSFormMeta } from '../../utils/models'
@ -11,52 +13,50 @@ interface ViewLibraryProps {
} }
function ViewLibrary({ schemas }: ViewLibraryProps) { function ViewLibrary({ schemas }: ViewLibraryProps) {
const { resetQuery: cleanQuery } = useNavSearch();
const navigate = useNavigate(); const navigate = useNavigate();
const intl = useIntl(); const intl = useIntl();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const openRSForm = (schema: IRSFormMeta) => { const openRSForm = (schema: IRSFormMeta) => navigate(`/rsforms/${schema.id}`);
navigate(`/rsforms/${schema.id}`);
};
const columns = useMemo(() => const columns = useMemo(() =>
[ [
{ {
name: 'Шифр', name: 'Шифр',
id: 'alias', id: 'alias',
maxWidth: '140px', maxWidth: '140px',
selector: (schema: IRSFormMeta) => schema.alias, selector: (schema: IRSFormMeta) => schema.alias,
sortable: true, sortable: true,
reorder: true reorder: true
},
{
name: 'Название',
id: 'title',
minWidth: '50%',
selector: (schema: IRSFormMeta) => schema.title,
sortable: true,
reorder: true
},
{
name: 'Владелец',
id: 'owner',
selector: (schema: IRSFormMeta) => schema.owner ?? 0,
format: (schema: IRSFormMeta) => {
return getUserLabel(schema.owner);
}, },
{ sortable: true,
name: 'Название', reorder: true
id: 'title', },
minWidth: '50%', {
selector: (schema: IRSFormMeta) => schema.title, name: 'Обновлена',
sortable: true, id: 'time_update',
reorder: true selector: (schema: IRSFormMeta) => schema.time_update,
}, format: (schema: IRSFormMeta) => new Date(schema.time_update).toLocaleString(intl.locale),
{ sortable: true,
name: 'Владелец', reorder: true
id: 'owner', }
selector: (schema: IRSFormMeta) => schema.owner ?? 0, ], [intl, getUserLabel]);
format: (schema: IRSFormMeta) => {
return getUserLabel(schema.owner);
},
sortable: true,
reorder: true
},
{
name: 'Обновлена',
id: 'time_update',
selector: (schema: IRSFormMeta) => schema.time_update,
format: (schema: IRSFormMeta) => new Date(schema.time_update).toLocaleString(intl.locale),
sortable: true,
reorder: true
}
], [intl, getUserLabel]
);
return ( return (
<ConceptDataTable <ConceptDataTable
@ -70,7 +70,15 @@ function ViewLibrary({ schemas }: ViewLibraryProps) {
noDataComponent={<span className='flex flex-col justify-center p-2 text-center'> noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
<p>Список схем пуст</p> <p>Список схем пуст</p>
<p>Измените фильтр или создайте новую концептуальную схему</p> <p>
<TextURL text='Создать схему' href='/rsform-create'/>
<span> | </span>
<TextURL text='Все схемы' href='/library?filter=common'/>
<span> | </span>
<span className='cursor-pointer hover:underline clr-url' onClick={cleanQuery}>
<b>Очистить фильтр</b>
</span>
</p>
</span>} </span>}
pagination pagination

View File

@ -11,7 +11,7 @@ import ViewLibrary from './ViewLibrary';
function LibraryPage() { function LibraryPage() {
const search = useLocation().search; const search = useLocation().search;
const { query, cleanQuery } = useNavSearch(); const { query, resetQuery: cleanQuery } = useNavSearch();
const { user } = useAuth(); const { user } = useAuth();
const library = useLibrary(); const library = useLibrary();
@ -21,10 +21,12 @@ function LibraryPage() {
useLayoutEffect(() => { useLayoutEffect(() => {
const filterType = new URLSearchParams(search).get('filter'); const filterType = new URLSearchParams(search).get('filter');
if (filterType === 'common') { if (filterType === 'common') {
cleanQuery();
setFilterParams({ setFilterParams({
is_common: true is_common: true
}); });
} else if (filterType === 'personal' && user) { } else if (filterType === 'personal' && user) {
cleanQuery();
setFilterParams({ setFilterParams({
ownedBy: user.id! ownedBy: user.id!
}); });

View File

@ -41,7 +41,7 @@ function LoginPage() {
return ( return (
<div className='w-full py-2'> { user <div className='w-full py-2'> { user
? <b>{`Вы вошли в систему как ${user.username}`}</b> ? <b>{`Вы вошли в систему как ${user.username}`}</b>
: <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[20rem]'> : <Form title='Ввод данных пользователя' onSubmit={handleSubmit} widthClass='w-[21rem]'>
<TextInput id='username' <TextInput id='username'
label='Имя пользователя' label='Имя пользователя'
required required

View File

@ -2,26 +2,51 @@ import { useEffect, useState } from 'react';
import ConceptSelect from '../../components/Common/ConceptSelect'; import ConceptSelect from '../../components/Common/ConceptSelect';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import { type CstType } from '../../utils/models'; import TextArea from '../../components/Common/TextArea';
import RSInput from '../../components/RSInput';
import { CstType,ICstCreateData } from '../../utils/models';
import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI'; import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
interface DlgCreateCstProps { interface DlgCreateCstProps {
hideWindow: () => void hideWindow: () => void
defaultType?: CstType initial?: ICstCreateData
onCreate: (type: CstType) => void onCreate: (data: ICstCreateData) => void
} }
function DlgCreateCst({ hideWindow, defaultType, onCreate }: DlgCreateCstProps) { function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [selectedType, setSelectedType] = useState<CstType | undefined>(undefined); const [selectedType, setSelectedType] = useState<CstType>(CstType.BASE);
const [term, setTerm] = useState('');
const [textDefinition, setTextDefinition] = useState('');
const [expression, setExpression] = useState('');
const [convention, setConvention] = useState('');
function getData(): ICstCreateData {
return {
cst_type: selectedType,
insert_after: initial?.insert_after ?? null,
alias: '',
convention: convention,
definition_formal: expression,
definition_raw: textDefinition,
term_raw: term
}
}
const handleSubmit = () => { const handleSubmit = () => {
if (selectedType) onCreate(selectedType); onCreate(getData());
}; };
useEffect(() => { useEffect(() => {
setSelectedType(defaultType); if (initial) {
}, [defaultType]); setSelectedType(initial.cst_type);
setTerm(initial.term_raw);
setTextDefinition(initial.definition_raw);
setExpression(initial.definition_formal);
setConvention(initial.convention);
}
}, [initial]);
useEffect(() => { useEffect(() => {
setValidated(selectedType !== undefined); setValidated(selectedType !== undefined);
@ -35,16 +60,45 @@ function DlgCreateCst({ hideWindow, defaultType, onCreate }: DlgCreateCstProps)
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='fixed h-fit w-[15rem] px-2'> <div className='h-fit w-[35rem] px-2 mb-2 flex flex-col justify-stretch'>
<div className='flex justify-center w-full'>
<ConceptSelect <ConceptSelect
className='my-4' className='my-2 min-w-[15rem] self-center'
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []} values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []}
onChange={data => { setSelectedType(data.length > 0 ? data[0].value : undefined); }} onChange={data => { setSelectedType(data.length > 0 ? data[0].value : CstType.BASE); }}
/>
</div>
<TextArea id='term' label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2}
value={term}
spellCheck
onChange={event => setTerm(event.target.value)}
/>
<RSInput id='expression' label='Формальное выражение'
editable
className='mt-2'
height='5.5rem'
value={expression}
onChange={value => setExpression(value)}
/>
<TextArea id='definition' label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
rows={2}
value={textDefinition}
spellCheck
onChange={event => { setTextDefinition(event.target.value); }}
/>
<TextArea id='convention' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={2}
value={convention}
spellCheck
onChange={event => { setConvention(event.target.value); }}
/> />
</div> </div>
<div className='h-[4rem]'></div>
</Modal> </Modal>
); );
} }

View File

@ -9,7 +9,7 @@ import TextArea from '../../components/Common/TextArea';
import CstStatusInfo from '../../components/Help/InfoCstStatus'; import CstStatusInfo from '../../components/Help/InfoCstStatus';
import { DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons'; import { DumpBinIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { type CstType, EditMode, ICstUpdateData, SyntaxTree } from '../../utils/models'; import { CstType, EditMode, ICstCreateData, ICstUpdateData, SyntaxTree } from '../../utils/models';
import { getCstTypeLabel, getCstTypificationLabel } from '../../utils/staticUI'; import { getCstTypeLabel, getCstTypificationLabel } from '../../utils/staticUI';
import EditorRSExpression from './EditorRSExpression'; import EditorRSExpression from './EditorRSExpression';
import ViewSideConstituents from './elements/ViewSideConstituents'; import ViewSideConstituents from './elements/ViewSideConstituents';
@ -21,7 +21,7 @@ interface EditorConstituentaProps {
activeID?: number activeID?: number
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onShowAST: (expression: string, ast: SyntaxTree) => void onShowAST: (expression: string, ast: SyntaxTree) => void
onCreateCst: (selectedID: number | undefined, type: CstType | undefined) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
} }
@ -101,7 +101,16 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
if (!activeID || !schema) { if (!activeID || !schema) {
return; return;
} }
onCreateCst(activeID, activeCst?.cstType); const data: ICstCreateData = {
insert_after: activeID,
cst_type: activeCst?.cstType ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: '',
definition_raw: '',
convention: '',
};
onCreateCst(data);
} }
function handleRename() { function handleRename() {

View File

@ -10,12 +10,12 @@ import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, Sm
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { CstType, IConstituenta, ICstMovetoData } from '../../utils/models' import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
import { getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI'; import { getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
interface EditorItemsProps { interface EditorItemsProps {
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
} }
@ -99,7 +99,16 @@ function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps)
return Math.max(position, prev); return Math.max(position, prev);
}, -1); }, -1);
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined; const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
onCreateCst(insert_where, type, type !== undefined); const data: ICstCreateData = {
insert_after: insert_where ?? null,
cst_type: type ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: '',
definition_raw: '',
convention: '',
};
onCreateCst(data, type !== undefined);
} }
// Implement hotkeys for working with constituents table // Implement hotkeys for working with constituents table

View File

@ -2,10 +2,9 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Label from '../../components/Common/Label';
import { Loader } from '../../components/Common/Loader'; import { Loader } from '../../components/Common/Loader';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import { getSymbolSubstitute, TextWrapper } from '../../components/RSInput/textEditing'; import { TextWrapper } from '../../components/RSInput/textEditing';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression'; import useCheckExpression from '../../hooks/useCheckExpression';
import { TokenID } from '../../utils/enums'; import { TokenID } from '../../utils/enums';
@ -32,8 +31,8 @@ interface EditorRSExpressionProps {
} }
function EditorRSExpression({ function EditorRSExpression({
id, activeCst, label, disabled, isActive, placeholder, value, onShowAST, activeCst, disabled, isActive, value, onShowAST,
toggleEditMode, setTypification, onChange toggleEditMode, setTypification, onChange, ... props
}: EditorRSExpressionProps) { }: EditorRSExpressionProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
@ -106,31 +105,6 @@ function EditorRSExpression({
setIsModified(true); setIsModified(true);
}, []); }, []);
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!rsInput.current) {
return;
}
const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
if (event.shiftKey && event.key === '*' && !event.altKey) {
text.insertToken(TokenID.DECART);
} else if (event.altKey) {
if (!text.processAltKey(event.key)) {
return;
}
} else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.key);
if (!newSymbol) {
return;
}
text.replaceWith(newSymbol);
} else {
return;
}
event.preventDefault();
setIsModified(true);
}, []);
const EditButtons = useMemo(() => { const EditButtons = useMemo(() => {
return (<div className='flex items-center justify-between w-full'> return (<div className='flex items-center justify-between w-full'>
<div className='text-sm w-fit'> <div className='text-sm w-fit'>
@ -220,20 +194,14 @@ function EditorRSExpression({
/> />
</div> </div>
</div> </div>
<Label
text={label}
required={false}
htmlFor={id}
/>
<RSInput innerref={rsInput} <RSInput innerref={rsInput}
className='mt-2' className='mt-2'
height='10.1rem' height='10.1rem'
value={value} value={value}
placeholder={placeholder}
editable={!disabled} editable={!disabled}
onChange={handleChange} onChange={handleChange}
onKeyDown={handleInput}
onFocus={handleFocusIn} onFocus={handleFocusIn}
{...props}
/> />
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'> <div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>

View File

@ -18,7 +18,7 @@ import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { prefixes, resources } from '../../utils/constants'; import { prefixes, resources } from '../../utils/constants';
import { Graph } from '../../utils/Graph'; import { Graph } from '../../utils/Graph';
import { CstType, IConstituenta } from '../../utils/models'; import { CstType, IConstituenta, ICstCreateData } from '../../utils/models';
import { getCstClassColor, getCstStatusColor, import { getCstClassColor, getCstStatusColor,
GraphColoringSelector, GraphLayoutSelector, GraphColoringSelector, GraphLayoutSelector,
mapColoringLabels, mapLayoutLabels mapColoringLabels, mapLayoutLabels
@ -57,7 +57,7 @@ export interface GraphEditorParams {
interface EditorTermGraphProps { interface EditorTermGraphProps {
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onCreateCst: (selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
} }
@ -270,12 +270,16 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
if (!schema) { if (!schema) {
return; return;
} }
const selectedPosition = allSelected.reduce((prev, cstID) => { const data: ICstCreateData = {
const position = schema.items.findIndex(cst => cst.id === Number(cstID)); insert_after: null,
return Math.max(position, prev); cst_type: allSelected.length == 0 ? CstType.BASE: CstType.TERM,
}, -1); alias: '',
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined; term_raw: '',
onCreateCst(insert_where, undefined); definition_formal: allSelected.map(id => schema.items.find(cst => cst.id === Number(id))!.alias).join(' '),
definition_raw: '',
convention: '',
};
onCreateCst(data);
} }
function handleDeleteCst() { function handleDeleteCst() {
@ -352,7 +356,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
initial={getOptions()} initial={getOptions()}
onConfirm={handleChangeOptions} onConfirm={handleChangeOptions}
/>} />}
<div className='flex flex-col py-2 border-t border-r max-w-[12.44rem] pr-2 text-sm select-none' style={{height: canvasHeight}}> <div className='flex flex-col border-t border-r max-w-[12.44rem] pr-2 pb-2 text-sm select-none' style={{height: canvasHeight}}>
{hoverCst && {hoverCst &&
<div className='relative'> <div className='relative'>
<InfoConstituenta <InfoConstituenta
@ -361,7 +365,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
/> />
</div>} </div>}
<div className='flex items-center justify-between'> <div className='flex items-center justify-between py-1'>
<div className='mr-3 whitespace-nowrap'> <div className='mr-3 whitespace-nowrap'>
Выбраны Выбраны
<span className='ml-1'> <span className='ml-1'>
@ -459,7 +463,7 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
className='relative border-t border-r' className='relative border-t border-r'
style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}} style={{width: canvasWidth, height: canvasHeight, borderBottomWidth: noNavigation ? '1px': ''}}
> >
<div className='relative top-0 right-0 z-10 flex m-2 flex-start'> <div className='relative top-0 right-0 z-10 flex mt-1 ml-2 flex-start'>
<div className='px-1 py-1' id='items-graph-help' > <div className='px-1 py-1' id='items-graph-help' >
<HelpIcon color='text-primary' size={5} /> <HelpIcon color='text-primary' size={5} />
</div> </div>

View File

@ -9,7 +9,7 @@ import { Loader } from '../../components/Common/Loader';
import { useLibrary } from '../../context/LibraryContext'; import { useLibrary } from '../../context/LibraryContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { CstType, ICstCreateData, SyntaxTree } from '../../utils/models'; import { ICstCreateData, SyntaxTree } from '../../utils/models';
import { createAliasFor } from '../../utils/staticUI'; import { createAliasFor } from '../../utils/staticUI';
import DlgCloneRSForm from './DlgCloneRSForm'; import DlgCloneRSForm from './DlgCloneRSForm';
import DlgCreateCst from './DlgCreateCst'; import DlgCreateCst from './DlgCreateCst';
@ -53,8 +53,7 @@ function RSTabs() {
const [toBeDeleted, setToBeDeleted] = useState<number[]>([]); const [toBeDeleted, setToBeDeleted] = useState<number[]>([]);
const [showDeleteCst, setShowDeleteCst] = useState(false); const [showDeleteCst, setShowDeleteCst] = useState(false);
const [defaultType, setDefaultType] = useState<CstType | undefined>(undefined); const [createInitialData, setCreateInitialData] = useState<ICstCreateData>();
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
const [showCreateCst, setShowCreateCst] = useState(false); const [showCreateCst, setShowCreateCst] = useState(false);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -90,15 +89,11 @@ function RSTabs() {
}, [navigate, schema, activeTab]); }, [navigate, schema, activeTab]);
const handleCreateCst = useCallback( const handleCreateCst = useCallback(
(type: CstType, selectedCst?: number) => { (data: ICstCreateData) => {
if (!schema?.items) { if (!schema?.items) {
return; return;
} }
const data: ICstCreateData = { data.alias = createAliasFor(data.cst_type, schema);
cst_type: type,
alias: createAliasFor(type, schema),
insert_after: selectedCst ?? insertWhere ?? null
}
cstCreate(data, newCst => { cstCreate(data, newCst => {
toast.success(`Конституента добавлена: ${newCst.alias}`); toast.success(`Конституента добавлена: ${newCst.alias}`);
navigateTo(activeTab, newCst.id); navigateTo(activeTab, newCst.id);
@ -115,15 +110,14 @@ function RSTabs() {
}, TIMEOUT_UI_REFRESH); }, TIMEOUT_UI_REFRESH);
} }
}); });
}, [schema, cstCreate, insertWhere, navigateTo, activeTab]); }, [schema, cstCreate, navigateTo, activeTab]);
const promptCreateCst = useCallback( const promptCreateCst = useCallback(
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => { (initialData: ICstCreateData, skipDialog?: boolean) => {
if (skipDialog && type) { if (skipDialog) {
handleCreateCst(type, selectedID); handleCreateCst(initialData);
} else { } else {
setDefaultType(type); setCreateInitialData(initialData);
setInsertWhere(selectedID);
setShowCreateCst(true); setShowCreateCst(true);
} }
}, [handleCreateCst]); }, [handleCreateCst]);
@ -209,7 +203,7 @@ function RSTabs() {
<DlgCreateCst <DlgCreateCst
hideWindow={() => setShowCreateCst(false)} hideWindow={() => setShowCreateCst(false)}
onCreate={handleCreateCst} onCreate={handleCreateCst}
defaultType={defaultType} initial={createInitialData}
/>} />}
{showDeleteCst && {showDeleteCst &&
<DlgDeleteCst <DlgDeleteCst

View File

@ -156,7 +156,8 @@ export interface IConstituentaList {
items: IConstituentaID[] items: IConstituentaID[]
} }
export interface ICstCreateData extends Pick<IConstituentaMeta, 'alias' | 'cst_type'> { export interface ICstCreateData
extends Pick<IConstituentaMeta, 'alias' | 'cst_type' | 'definition_raw' | 'term_raw' | 'convention' | 'definition_formal' > {
insert_after: number | null insert_after: number | null
} }

View File

@ -485,7 +485,7 @@ export function getRSErrorPrefix(error: IRSErrorDescription): string {
export function getRSErrorMessage(error: IRSErrorDescription): string { export function getRSErrorMessage(error: IRSErrorDescription): string {
switch (error.errorType) { switch (error.errorType) {
case RSErrorType.syntax: case RSErrorType.syntax:
return 'UNKNOWN SYNTAX ERROR'; return 'Неопределенная синтаксическая ошибка';
case RSErrorType.missingParanthesis: case RSErrorType.missingParanthesis:
return 'Некорректная конструкция языка родов структур, проверьте структуру выражения'; return 'Некорректная конструкция языка родов структур, проверьте структуру выражения';
case RSErrorType.missingCurlyBrace: case RSErrorType.missingCurlyBrace: