UI fixes and synthesis table unification

This commit is contained in:
IRBorisov 2024-03-27 22:54:24 +03:00
parent 54da2f4871
commit 102f8c2baf
24 changed files with 208 additions and 157 deletions

View File

@ -1,12 +1,12 @@
''' Utility: Graph implementation. ''' ''' Utility: Graph implementation. '''
from typing import Dict, Iterable, Optional, cast from typing import Iterable, Optional, cast
class Graph: class Graph:
''' Directed graph. ''' ''' Directed graph. '''
def __init__(self, graph: Optional[Dict[str, list[str]]]=None): def __init__(self, graph: Optional[dict[str, list[str]]]=None):
if graph is None: if graph is None:
self._graph = cast(Dict[str, list[str]], {}) self._graph = cast(dict[str, list[str]], {})
else: else:
self._graph = graph self._graph = graph

View File

@ -16,6 +16,9 @@ def renameTrivial(name: str):
def substituteTrivial(name: str): def substituteTrivial(name: str):
return f'Отождествление конституенты с собой не корректно: {name}' return f'Отождествление конституенты с собой не корректно: {name}'
def substituteDouble(name: str):
return f'Повторное отождествление: {name}'
def aliasTaken(name: str): def aliasTaken(name: str):
return f'Имя уже используется: {name}' return f'Имя уже используется: {name}'

View File

@ -1,5 +1,5 @@
''' Models: RSForm API. ''' ''' Models: RSForm API. '''
from typing import Dict, Iterable, Optional, Union, cast from typing import Iterable, Optional, Union, cast
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@ -53,6 +53,7 @@ class RSForm:
''' Trigger cascade resolutions when term changes. ''' ''' Trigger cascade resolutions when term changes. '''
graph_terms = self._term_graph() graph_terms = self._term_graph()
expansion = graph_terms.expand_outputs(changed) expansion = graph_terms.expand_outputs(changed)
expanded_change = list(changed) + expansion
resolver = self.resolver() resolver = self.resolver()
if len(expansion) > 0: if len(expansion) > 0:
for alias in graph_terms.topological_order(): for alias in graph_terms.topological_order():
@ -67,7 +68,7 @@ class RSForm:
resolver.context[cst.alias] = Entity(cst.alias, resolved) resolver.context[cst.alias] = Entity(cst.alias, resolved)
graph_defs = self._definition_graph() graph_defs = self._definition_graph()
update_defs = set(expansion + graph_defs.expand_outputs(expansion + changed)).union(changed) update_defs = set(expansion + graph_defs.expand_outputs(expanded_change)).union(changed)
if len(update_defs) == 0: if len(update_defs) == 0:
return return
for alias in update_defs: for alias in update_defs:
@ -126,11 +127,11 @@ class RSForm:
position = self._get_insert_position(position) position = self._get_insert_position(position)
self._shift_positions(position, count) self._shift_positions(position, count)
indices: Dict[str, int] = {} indices: dict[str, int] = {}
for (value, _) in CstType.choices: for (value, _) in CstType.choices:
indices[value] = self.get_max_index(cast(CstType, value)) indices[value] = self.get_max_index(cast(CstType, value))
mapping: Dict[str, str] = {} mapping: dict[str, str] = {}
for cst in items: for cst in items:
indices[cst.cst_type] = indices[cst.cst_type] + 1 indices[cst.cst_type] = indices[cst.cst_type] + 1
newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}' newAlias = f'{get_type_prefix(cst.cst_type)}{indices[cst.cst_type]}'

View File

@ -194,34 +194,6 @@ class RSFormParseSerializer(serializers.ModelSerializer):
return data return data
class CstSubstituteSerializerBase(serializers.Serializer):
''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.all())
substitution = PKField(many=False, queryset=Constituenta.objects.all())
transfer_term = serializers.BooleanField(required=False, default=False)
class CstSubstituteSerializer(CstSubstituteSerializerBase):
''' Serializer: Constituenta substitution. '''
def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema'])
original_cst = cast(Constituenta, attrs['original'])
substitution_cst = cast(Constituenta, attrs['substitution'])
if original_cst.alias == substitution_cst.alias:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
if original_cst.schema != schema:
raise serializers.ValidationError({
'original': msg.constituentaNotOwned(schema.title)
})
if substitution_cst.schema != schema:
raise serializers.ValidationError({
'substitution': msg.constituentaNotOwned(schema.title)
})
return attrs
class CstTargetSerializer(serializers.Serializer): class CstTargetSerializer(serializers.Serializer):
''' Serializer: Target single Constituenta. ''' ''' Serializer: Target single Constituenta. '''
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.all())
@ -289,6 +261,46 @@ class CstMoveSerializer(CstListSerializer):
move_to = serializers.IntegerField() move_to = serializers.IntegerField()
class CstSubstituteSerializerBase(serializers.Serializer):
''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.all())
substitution = PKField(many=False, queryset=Constituenta.objects.all())
transfer_term = serializers.BooleanField(required=False, default=False)
class CstSubstituteSerializer(serializers.Serializer):
''' Serializer: Constituenta substitution. '''
substitutions = serializers.ListField(
child=CstSubstituteSerializerBase(),
min_length=1
)
def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema'])
deleted = set()
for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution'])
if original_cst.pk in deleted:
raise serializers.ValidationError({
f'{original_cst.id}': msg.substituteDouble(original_cst.alias)
})
if original_cst.alias == substitution_cst.alias:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
if original_cst.schema != schema:
raise serializers.ValidationError({
'original': msg.constituentaNotOwned(schema.title)
})
if substitution_cst.schema != schema:
raise serializers.ValidationError({
'substitution': msg.constituentaNotOwned(schema.title)
})
deleted.add(original_cst.pk)
return attrs
class InlineSynthesisSerializer(serializers.Serializer): class InlineSynthesisSerializer(serializers.Serializer):
''' Serializer: Inline synthesis operation input. ''' ''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=LibraryItem.objects.all()) receiver = PKField(many=False, queryset=LibraryItem.objects.all())
@ -313,6 +325,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.id}': msg.constituentaNotOwned(schema_in.title) f'{cst.id}': msg.constituentaNotOwned(schema_in.title)
}) })
deleted = set()
for item in attrs['substitutions']: for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original']) original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution']) substitution_cst = cast(Constituenta, item['substitution'])
@ -334,4 +347,9 @@ class InlineSynthesisSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title) f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title)
}) })
if original_cst.pk in deleted:
raise serializers.ValidationError({
f'{original_cst.id}': msg.substituteDouble(original_cst.alias)
})
deleted.add(original_cst.pk)
return attrs return attrs

View File

@ -260,7 +260,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch') @decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_constituenta(self): def test_substitute_single(self):
x1 = self.schema.insert_new( x1 = self.schema.insert_new(
alias='X1', alias='X1',
term_raw='Test1', term_raw='Test1',
@ -273,14 +273,14 @@ class TestRSFormViewset(EndpointTester):
) )
unowned = self.unowned.insert_new('X2') unowned = self.unowned.insert_new('X2')
data = {'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True} data = {'substitutions': [{'original': x1.pk, 'substitution': unowned.pk, 'transfer_term': True}]}
self.assertForbidden(data, item=self.unowned_id) self.assertForbidden(data, item=self.unowned_id)
self.assertBadData(data, item=self.schema_id) self.assertBadData(data, item=self.schema_id)
data = {'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True} data = {'substitutions': [{'original': unowned.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.assertBadData(data, item=self.schema_id) self.assertBadData(data, item=self.schema_id)
data = {'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True} data = {'substitutions': [{'original': x1.pk, 'substitution': x1.pk, 'transfer_term': True}]}
self.assertBadData(data, item=self.schema_id) self.assertBadData(data, item=self.schema_id)
d1 = self.schema.insert_new( d1 = self.schema.insert_new(
@ -288,7 +288,7 @@ class TestRSFormViewset(EndpointTester):
term_raw='@{X2|sing,datv}', term_raw='@{X2|sing,datv}',
definition_formal='X1' definition_formal='X1'
) )
data = {'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True} data = {'substitutions': [{'original': x1.pk, 'substitution': x2.pk, 'transfer_term': True}]}
response = self.execute(data, item=self.schema_id) response = self.execute(data, item=self.schema_id)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -298,6 +298,53 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(d1.term_resolved, 'form1') self.assertEqual(d1.term_resolved, 'form1')
self.assertEqual(d1.definition_formal, 'X2') self.assertEqual(d1.definition_formal, 'X2')
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_multiple(self):
self.set_params(item=self.schema_id)
x1 = self.schema.insert_new('X1')
x2 = self.schema.insert_new('X2')
d1 = self.schema.insert_new('D1')
d2 = self.schema.insert_new('D2')
d3 = self.schema.insert_new(
alias='D3',
definition_formal='X1 \ X2'
)
data = {'substitutions': []}
self.assertBadData(data)
data = {'substitutions': [
{
'original': x1.pk,
'substitution': d1.pk,
'transfer_term': True
},
{
'original': x1.pk,
'substitution': d2.pk,
'transfer_term': True
}
]}
self.assertBadData(data)
data = {'substitutions': [
{
'original': x1.pk,
'substitution': d1.pk,
'transfer_term': True
},
{
'original': x2.pk,
'substitution': d2.pk,
'transfer_term': True
}
]}
response = self.execute(data, item=self.schema_id)
self.assertEqual(response.status_code, status.HTTP_200_OK)
d3.refresh_from_db()
self.assertEqual(d3.definition_formal, 'D1 \ D2')
@decl_endpoint('/api/rsforms/{item}/cst-create', method='post') @decl_endpoint('/api/rsforms/{item}/cst-create', method='post')
def test_create_constituenta_data(self): def test_create_constituenta_data(self):

View File

@ -147,11 +147,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
context={'schema': schema.item} context={'schema': schema.item}
) )
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
schema.substitute( for substitution in serializer.validated_data['substitutions']:
original=serializer.validated_data['original'], original = cast(m.Constituenta, substitution['original'])
substitution=serializer.validated_data['substitution'], replacement = cast(m.Constituenta, substitution['substitution'])
transfer_term=serializer.validated_data['transfer_term'] schema.substitute(original, replacement, substitution['transfer_term'])
)
schema.item.refresh_from_db() schema.item.refresh_from_db()
return Response( return Response(
status=c.HTTP_200_OK, status=c.HTTP_200_OK,

View File

@ -1,5 +1,5 @@
''' Term context for reference resolution. ''' ''' Term context for reference resolution. '''
from typing import Iterable, Dict, Optional, TypedDict from typing import Iterable, Optional, TypedDict
from .ruparser import PhraseParser from .ruparser import PhraseParser
from .rumodel import WordTag from .rumodel import WordTag
@ -81,4 +81,4 @@ class Entity:
# Represents term context for resolving entity references. # Represents term context for resolving entity references.
TermContext = Dict[str, Entity] TermContext = dict[str, Entity]

View File

@ -2,7 +2,7 @@ function HelpRSTemplates() {
// prettier-ignore // prettier-ignore
return ( return (
<div> <div>
<h1>Банк выражений</h1> <h1>Шаблоны</h1>
<p>Портал предоставляет быстрый доступ к часто используемым выражениям с помощью функции создания конституенты из шаблона</p> <p>Портал предоставляет быстрый доступ к часто используемым выражениям с помощью функции создания конституенты из шаблона</p>
<p>Источником шаблонов является <b>Банк выражений</b>, содержащий параметризованные понятия и утверждения, сгруппированные по разделам</p> <p>Источником шаблонов является <b>Банк выражений</b>, содержащий параметризованные понятия и утверждения, сгруппированные по разделам</p>
<p>Сначала выбирается шаблон выражения (вкладка Шаблон)</p> <p>Сначала выбирается шаблон выражения (вкладка Шаблон)</p>

View File

@ -100,15 +100,15 @@ function ConstituentaMultiPicker({ id, schema, prefixID, rows, selected, setSele
</span> </span>
<div className='flex w-full gap-6 text-sm'> <div className='flex w-full gap-6 text-sm'>
<Button <Button
text='Поставщики' text='Влияющие'
title='Добавить все конституенты, от которых зависят выбранные' title='Добавить все конституенты, от которых зависят выбранные'
className='w-[7rem]' className='w-[7rem] text-sm'
onClick={selectBasis} onClick={selectBasis}
/> />
<Button <Button
text='Потребители' text='Зависимые'
title='Добавить все конституенты, которые зависят от выбранных' title='Добавить все конституенты, которые зависят от выбранных'
className='w-[7rem]' className='w-[7rem] text-sm'
onClick={selectDependant} onClick={selectDependant}
/> />
</div> </div>

View File

@ -154,11 +154,11 @@ function SubstitutionsPicker({
); );
return ( return (
<div className='flex flex-col'> <div className='flex flex-col w-full'>
<div className='flex items-end gap-3 justify-stretch'> <div className='flex items-end gap-3 justify-stretch'>
<div className='flex-grow basis-1/2'> <div className='flex-grow basis-1/2'>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Label text={schema1?.alias ?? 'Схема 1'} /> <Label text={schema1 !== schema2 ? schema1?.alias ?? 'Схема 1' : ''} />
<div> <div>
<MiniButton <MiniButton
title='Сохранить конституенту' title='Сохранить конституенту'
@ -204,7 +204,7 @@ function SubstitutionsPicker({
<div className='flex-grow basis-1/2'> <div className='flex-grow basis-1/2'>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Label text={schema2?.alias ?? 'Схема 2'} /> <Label text={schema1 !== schema2 ? schema2?.alias ?? 'Схема 2' : ''} />
<div> <div>
<MiniButton <MiniButton
title='Сохранить конституенту' title='Сохранить конституенту'
@ -242,8 +242,9 @@ function SubstitutionsPicker({
<DataTable <DataTable
dense dense
noHeader
noFooter noFooter
className='overflow-y-auto border select-none' className='w-full overflow-y-auto text-sm border select-none'
rows={rows} rows={rows}
contentHeight='1.3rem' contentHeight='1.3rem'
data={items} data={items}

View File

@ -7,11 +7,13 @@ import { CProps } from '../props';
interface MiniButtonProps extends CProps.Button { interface MiniButtonProps extends CProps.Button {
icon: React.ReactNode; icon: React.ReactNode;
noHover?: boolean; noHover?: boolean;
noPadding?: boolean;
} }
function MiniButton({ function MiniButton({
icon, icon,
noHover, noHover,
noPadding,
tabIndex, tabIndex,
title, title,
titleHtml, titleHtml,
@ -24,11 +26,11 @@ function MiniButton({
type='button' type='button'
tabIndex={tabIndex ?? -1} tabIndex={tabIndex ?? -1}
className={clsx( className={clsx(
'px-1 py-1',
'rounded-full', 'rounded-full',
'clr-btn-clear', 'clr-btn-clear',
'cursor-pointer disabled:cursor-not-allowed', 'cursor-pointer disabled:cursor-not-allowed',
{ {
'px-1 py-1': !noPadding,
'outline-none': noHover, 'outline-none': noHover,
'clr-hover': !noHover 'clr-hover': !noHover
}, },

View File

@ -71,8 +71,9 @@ function Modal({
exit={{ ...animateModal.exit }} exit={{ ...animateModal.exit }}
{...restProps} {...restProps}
> >
<Overlay position='right-[0.3rem] top-2'> <Overlay position='right-2 top-2'>
<MiniButton <MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')} titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<BiX size='1.25rem' />} icon={<BiX size='1.25rem' />}
onClick={handleCancel} onClick={handleCancel}

View File

@ -34,7 +34,7 @@ import {
patchProduceStructure, patchProduceStructure,
patchRenameConstituenta, patchRenameConstituenta,
patchResetAliases, patchResetAliases,
patchSubstituteConstituenta, patchSubstituteConstituents,
patchUploadTRS, patchUploadTRS,
patchVersion, patchVersion,
postClaimLibraryItem, postClaimLibraryItem,
@ -374,7 +374,7 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
const cstSubstitute = useCallback( const cstSubstitute = useCallback(
(data: ICstSubstituteData, callback?: () => void) => { (data: ICstSubstituteData, callback?: () => void) => {
setError(undefined); setError(undefined);
patchSubstituteConstituenta(schemaID, { patchSubstituteConstituents(schemaID, {
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,

View File

@ -35,10 +35,6 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
const [argumentValue, setArgumentValue] = useState(''); const [argumentValue, setArgumentValue] = useState('');
const selectedClearable = useMemo(() => {
return argumentValue && !!selectedArgument && !!selectedArgument.value;
}, [argumentValue, selectedArgument]);
const isModified = useMemo( const isModified = useMemo(
() => selectedArgument && argumentValue !== selectedArgument.value, () => selectedArgument && argumentValue !== selectedArgument.value,
[selectedArgument, argumentValue] [selectedArgument, argumentValue]
@ -92,7 +88,6 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
() => [ () => [
argumentsHelper.accessor('alias', { argumentsHelper.accessor('alias', {
id: 'alias', id: 'alias',
header: 'Имя',
size: 40, size: 40,
minSize: 40, minSize: 40,
maxSize: 40, maxSize: 40,
@ -100,14 +95,12 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
}), }),
argumentsHelper.accessor(arg => arg.value || 'свободный аргумент', { argumentsHelper.accessor(arg => arg.value || 'свободный аргумент', {
id: 'value', id: 'value',
header: 'Значение',
size: 200, size: 200,
minSize: 200, minSize: 200,
maxSize: 200 maxSize: 200
}), }),
argumentsHelper.accessor(arg => arg.typification, { argumentsHelper.accessor(arg => arg.typification, {
id: 'type', id: 'type',
header: 'Типизация',
enableHiding: true, enableHiding: true,
cell: props => ( cell: props => (
<div <div
@ -122,16 +115,14 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
}), }),
argumentsHelper.display({ argumentsHelper.display({
id: 'actions', id: 'actions',
size: 50,
minSize: 50,
maxSize: 50,
cell: props => ( cell: props => (
<div className='max-h-[1.2rem]'> <div className='h-[1.25rem] w-[1.25rem]'>
{props.row.original.value ? ( {props.row.original.value ? (
<MiniButton <MiniButton
title='Очистить значение' title='Очистить значение'
icon={<BiX size='0.75rem' className='icon-red' />} noPadding
noHover noHover
icon={<BiX size='1.25rem' className='icon-red' />}
onClick={() => handleClearArgument(props.row.original)} onClick={() => handleClearArgument(props.row.original)}
/> />
) : null} ) : null}
@ -157,6 +148,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
<DataTable <DataTable
dense dense
noFooter noFooter
noHeader
className={clsx( className={clsx(
'max-h-[5.8rem] min-h-[5.8rem]', // prettier: split lines 'max-h-[5.8rem] min-h-[5.8rem]', // prettier: split lines
'overflow-y-auto', 'overflow-y-auto',
@ -194,21 +186,19 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
<div className='flex'> <div className='flex'>
<MiniButton <MiniButton
title='Подставить значение аргумента' title='Подставить значение аргумента'
icon={<BiCheck size='1.25rem' className='icon-green' />} noHover
className='py-0'
icon={<BiCheck size='2rem' className='icon-green' />}
disabled={!argumentValue || !selectedArgument} disabled={!argumentValue || !selectedArgument}
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)} onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
/> />
<MiniButton <MiniButton
title='Откатить значение' title='Очистить поле'
noHover
className='py-0'
disabled={!isModified} disabled={!isModified}
onClick={handleReset} onClick={handleReset}
icon={<BiRefresh size='1.25rem' className='icon-primary' />} icon={<BiRefresh size='2rem' className='icon-primary' />}
/>
<MiniButton
title='Очистить значение аргумента'
disabled={!selectedClearable}
icon={<BiX size='1.25rem' className='icon-red' />}
onClick={() => (selectedArgument ? handleClearArgument(selectedArgument) : undefined)}
/> />
</div> </div>
</div> </div>

View File

@ -114,7 +114,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Overlay position='top-0 right-[6rem]'> <Overlay position='top-0 right-[6rem]'>
<HelpButton topic={HelpTopic.RSTEMPLATES} className='max-w-[35rem]' /> <HelpButton topic={HelpTopic.RSTEMPLATES} className='max-w-[40rem]' offset={12} />
</Overlay> </Overlay>
<Tabs <Tabs
forceRenderTabPanel forceRenderTabPanel

View File

@ -60,13 +60,16 @@ function VersionsTable({ processing, items, onDelete, selected, onSelect }: Vers
minSize: 50, minSize: 50,
maxSize: 50, maxSize: 50,
cell: props => ( cell: props => (
<MiniButton <div className='h-[1.25rem] w-[1.25rem]'>
noHover <MiniButton
title='Удалить версию' title='Удалить версию'
disabled={processing} noHover
icon={<BiX size='1rem' className='icon-red' />} noPadding
onClick={() => onDelete(props.row.original.id)} disabled={processing}
/> icon={<BiX size='1.25rem' className='icon-red' />}
onClick={() => onDelete(props.row.original.id)}
/>
</div>
) )
}) })
], ],

View File

@ -129,7 +129,7 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='flex flex-col w-[40rem] px-6' className='flex flex-col w-[40rem] px-6'
> >
<Overlay position='top-[-0.2rem] left-[7.5rem]'> <Overlay position='top-[-0.2rem] left-[8rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} className='max-w-[38rem]' offset={3} /> <HelpButton topic={HelpTopic.TERM_CONTROL} className='max-w-[38rem]' offset={3} />
</Overlay> </Overlay>
@ -182,14 +182,14 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton <MiniButton
noHover noHover
title='Внести словоформу' title='Внести словоформу'
icon={<BiCheck size='1.25rem' className='icon-green' />} icon={<BiCheck size='1.5rem' className='icon-green' />}
disabled={textProcessor.loading || !inputText || inputGrams.length == 0} disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton
noHover noHover
title='Генерировать стандартные словоформы' title='Генерировать стандартные словоформы'
icon={<BiChevronsDown size='1.25rem' className='icon-primary' />} icon={<BiChevronsDown size='1.5rem' className='icon-primary' />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleGenerateLexeme} onClick={handleGenerateLexeme}
/> />
@ -200,7 +200,8 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton <MiniButton
noHover noHover
title='Сбросить все словоформы' title='Сбросить все словоформы'
icon={<BiX size='1rem' className='icon-red' />} className='py-0'
icon={<BiX size='1.5rem' className='icon-red' />}
disabled={textProcessor.loading || forms.length === 0} disabled={textProcessor.loading || forms.length === 0}
onClick={handleResetAll} onClick={handleResetAll}
/> />

View File

@ -39,16 +39,14 @@ function WordFormsTable({ forms, setForms, onFormSelect }: WordFormsTableProps)
id: 'text', id: 'text',
header: 'Текст', header: 'Текст',
size: 350, size: 350,
minSize: 350, minSize: 500,
maxSize: 350, maxSize: 500,
cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div> cell: props => <div className='min-w-[20rem]'>{props.getValue()}</div>
}), }),
columnHelper.accessor('grams', { columnHelper.accessor('grams', {
id: 'grams', id: 'grams',
header: 'Граммемы', header: 'Граммемы',
size: 250, maxSize: 150,
minSize: 250,
maxSize: 250,
cell: props => <WordFormBadge keyPrefix={props.cell.id} form={props.row.original} /> cell: props => <WordFormBadge keyPrefix={props.cell.id} form={props.row.original} />
}), }),
columnHelper.display({ columnHelper.display({
@ -57,12 +55,15 @@ function WordFormsTable({ forms, setForms, onFormSelect }: WordFormsTableProps)
minSize: 50, minSize: 50,
maxSize: 50, maxSize: 50,
cell: props => ( cell: props => (
<MiniButton <div className='h-[1.25rem] w-[1.25rem]'>
noHover <MiniButton
title='Удалить словоформу' noHover
icon={<BiX size='1rem' className='icon-red' />} noPadding
onClick={() => handleDeleteRow(props.row.index)} title='Удалить словоформу'
/> icon={<BiX size='1.25rem' className='icon-red' />}
onClick={() => handleDeleteRow(props.row.index)}
/>
</div>
) )
}) })
], ],
@ -73,7 +74,7 @@ function WordFormsTable({ forms, setForms, onFormSelect }: WordFormsTableProps)
<DataTable <DataTable
dense dense
noFooter noFooter
className={clsx('mb-2', 'max-h-[17.4rem] min-h-[17.4rem]', 'border', 'overflow-y-auto')} className={clsx('mb-2', 'max-h-[17.4rem] min-h-[17.4rem]', 'border', 'text-sm', 'overflow-y-auto')}
data={forms} data={forms}
columns={columns} columns={columns}
headPosition='0' headPosition='0'

View File

@ -2,15 +2,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { LuReplace } from 'react-icons/lu';
import ConstituentaSelector from '@/components/select/ConstituentaSelector'; import SubstitutionsPicker from '@/components/select/SubstitutionsPicker';
import Checkbox from '@/components/ui/Checkbox';
import FlexColumn from '@/components/ui/FlexColumn';
import Label from '@/components/ui/Label';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { IConstituenta, ICstSubstituteData } from '@/models/rsform'; import { ICstSubstituteData, ISubstitution } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> { interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
onSubstitute: (data: ICstSubstituteData) => void; onSubstitute: (data: ICstSubstituteData) => void;
@ -19,59 +16,38 @@ interface DlgSubstituteCstProps extends Pick<ModalProps, 'hideWindow'> {
function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) { function DlgSubstituteCst({ hideWindow, onSubstitute }: DlgSubstituteCstProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
const [original, setOriginal] = useState<IConstituenta | undefined>(undefined); const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
const [substitution, setSubstitution] = useState<IConstituenta | undefined>(undefined);
const [transferTerm, setTransferTerm] = useState(false);
const canSubmit = useMemo(() => { const canSubmit = useMemo(() => substitutions.length > 0, [substitutions]);
return !!original && !!substitution && substitution.id !== original.id;
}, [original, substitution]);
function handleSubmit() { function handleSubmit() {
const data: ICstSubstituteData = { const data: ICstSubstituteData = {
original: original!.id, substitutions: substitutions.map(item => ({
substitution: substitution!.id, original: item.deleteRight ? item.rightCst.id : item.leftCst.id,
transfer_term: transferTerm substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id,
transfer_term: !item.deleteRight && item.takeLeftTerm
}))
}; };
onSubstitute(data); onSubstitute(data);
} }
return ( return (
<Modal <Modal
header='Отождествление конституенты' header='Отождествление'
submitText='Отождествить' submitText='Отождествить'
submitInvalidTooltip={'Выберите две различные конституенты'} submitInvalidTooltip={'Выберите две различные конституенты'}
hideWindow={hideWindow} hideWindow={hideWindow}
canSubmit={canSubmit} canSubmit={canSubmit}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className={clsx('w-[25rem]', 'px-6 py-3 flex flex-col gap-3 justify-center items-center')} className={clsx('w-[40rem]', 'px-6 pb-3')}
> >
<FlexColumn> <SubstitutionsPicker
<Label text='Удаляемая конституента' /> items={substitutions}
<ConstituentaSelector setItems={setSubstitutions}
className='w-[20rem]' rows={6}
items={schema?.items} prefixID={prefixes.dlg_cst_substitutes_list}
value={original} schema1={schema}
onSelectValue={setOriginal} schema2={schema}
/>
</FlexColumn>
<LuReplace size='3rem' className='icon-primary' />
<FlexColumn>
<Label text='Подставляемая конституента' />
<ConstituentaSelector
className='w-[20rem]'
items={schema?.items}
value={substitution}
onSelectValue={setSubstitution}
/>
</FlexColumn>
<Checkbox
className='mt-3'
label='Сохранить термин удаляемой конституенты'
value={transferTerm}
setValue={setTransferTerm}
/> />
</Modal> </Modal>
); );

View File

@ -148,14 +148,21 @@ export interface ICstUpdateData
export interface ICstRenameData extends ICstTarget, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {} export interface ICstRenameData extends ICstTarget, Pick<IConstituentaMeta, 'alias' | 'cst_type'> {}
/** /**
* Represents data, used in merging {@link IConstituenta}. * Represents data, used in merging single {@link IConstituenta}.
*/ */
export interface ICstSubstituteData { export interface ICstSubstitute {
original: ConstituentaID; original: ConstituentaID;
substitution: ConstituentaID; substitution: ConstituentaID;
transfer_term: boolean; transfer_term: boolean;
} }
/**
* Represents data, used in merging multiple {@link IConstituenta}.
*/
export interface ICstSubstituteData {
substitutions: ICstSubstitute[];
}
/** /**
* Represents single substitution for synthesis table. * Represents single substitution for synthesis table.
*/ */
@ -251,5 +258,5 @@ export interface IInlineSynthesisData {
receiver: LibraryItemID; receiver: LibraryItemID;
source: LibraryItemID; source: LibraryItemID;
items: ConstituentaID[]; items: ConstituentaID[];
substitutions: ICstSubstituteData[]; substitutions: ICstSubstitute[];
} }

View File

@ -209,7 +209,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
text='Банк выражений' text='Шаблоны'
title='Создать конституенту из шаблона' title='Создать конституенту из шаблона'
icon={<BiDiamond size='1rem' className='icon-green' />} icon={<BiDiamond size='1rem' className='icon-green' />}
onClick={handleTemplates} onClick={handleTemplates}

View File

@ -334,9 +334,9 @@ export function patchProduceStructure(schema: string, request: FrontExchange<ICs
}); });
} }
export function patchSubstituteConstituenta(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) { export function patchSubstituteConstituents(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) {
AxiosPatch({ AxiosPatch({
title: `Substitution for constituenta id=${request.data.original} for schema id=${schema}`, title: `Substitution for constituents schema id=${schema}`,
endpoint: `/api/rsforms/${schema}/cst-substitute`, endpoint: `/api/rsforms/${schema}/cst-substitute`,
request: request request: request
}); });

View File

@ -133,5 +133,6 @@ export const prefixes = {
topic_list: 'topic_list_', topic_list: 'topic_list_',
library_list: 'library_list_', library_list: 'library_list_',
wordform_list: 'wordform_list_', wordform_list: 'wordform_list_',
rsedit_btn: 'rsedit_btn_' rsedit_btn: 'rsedit_btn_',
dlg_cst_substitutes_list: 'dlg_cst_substitutes_list_'
}; };

View File

@ -345,11 +345,11 @@ export function labelHelpTopic(topic: HelpTopic): string {
switch (topic) { switch (topic) {
case HelpTopic.MAIN: return 'Портал'; case HelpTopic.MAIN: return 'Портал';
case HelpTopic.LIBRARY: return 'Библиотека'; case HelpTopic.LIBRARY: return 'Библиотека';
case HelpTopic.RSFORM: return '- паспорт схемы'; case HelpTopic.RSFORM: return '- карточка схемы';
case HelpTopic.CSTLIST: return '- список конституент'; case HelpTopic.CSTLIST: return '- список конституент';
case HelpTopic.CONSTITUENTA: return '- конституента'; case HelpTopic.CONSTITUENTA: return '- конституента';
case HelpTopic.GRAPH_TERM: return '- граф термов'; case HelpTopic.GRAPH_TERM: return '- граф термов';
case HelpTopic.RSTEMPLATES: return '- Банк выражений'; case HelpTopic.RSTEMPLATES: return '- шаблоны выражений';
case HelpTopic.RSLANG: return 'Экспликация'; case HelpTopic.RSLANG: return 'Экспликация';
case HelpTopic.TERM_CONTROL: return 'Терминологизация'; case HelpTopic.TERM_CONTROL: return 'Терминологизация';
case HelpTopic.VERSIONS: return 'Версионирование'; case HelpTopic.VERSIONS: return 'Версионирование';