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. '''
from typing import Dict, Iterable, Optional, cast
from typing import Iterable, Optional, cast
class 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:
self._graph = cast(Dict[str, list[str]], {})
self._graph = cast(dict[str, list[str]], {})
else:
self._graph = graph

View File

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

View File

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

View File

@ -194,34 +194,6 @@ class RSFormParseSerializer(serializers.ModelSerializer):
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):
''' Serializer: Target single Constituenta. '''
target = PKField(many=False, queryset=Constituenta.objects.all())
@ -289,6 +261,46 @@ class CstMoveSerializer(CstListSerializer):
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):
''' Serializer: Inline synthesis operation input. '''
receiver = PKField(many=False, queryset=LibraryItem.objects.all())
@ -313,6 +325,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
raise serializers.ValidationError({
f'{cst.id}': msg.constituentaNotOwned(schema_in.title)
})
deleted = set()
for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution'])
@ -334,4 +347,9 @@ class InlineSynthesisSerializer(serializers.Serializer):
raise serializers.ValidationError({
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

View File

@ -260,7 +260,7 @@ class TestRSFormViewset(EndpointTester):
@decl_endpoint('/api/rsforms/{item}/cst-substitute', method='patch')
def test_substitute_constituenta(self):
def test_substitute_single(self):
x1 = self.schema.insert_new(
alias='X1',
term_raw='Test1',
@ -273,14 +273,14 @@ class TestRSFormViewset(EndpointTester):
)
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.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)
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)
d1 = self.schema.insert_new(
@ -288,7 +288,7 @@ class TestRSFormViewset(EndpointTester):
term_raw='@{X2|sing,datv}',
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)
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.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')
def test_create_constituenta_data(self):

View File

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

View File

@ -1,5 +1,5 @@
''' Term context for reference resolution. '''
from typing import Iterable, Dict, Optional, TypedDict
from typing import Iterable, Optional, TypedDict
from .ruparser import PhraseParser
from .rumodel import WordTag
@ -81,4 +81,4 @@ class Entity:
# 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
return (
<div>
<h1>Банк выражений</h1>
<h1>Шаблоны</h1>
<p>Портал предоставляет быстрый доступ к часто используемым выражениям с помощью функции создания конституенты из шаблона</p>
<p>Источником шаблонов является <b>Банк выражений</b>, содержащий параметризованные понятия и утверждения, сгруппированные по разделам</p>
<p>Сначала выбирается шаблон выражения (вкладка Шаблон)</p>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -114,7 +114,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
onSubmit={handleSubmit}
>
<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>
<Tabs
forceRenderTabPanel

View File

@ -60,13 +60,16 @@ function VersionsTable({ processing, items, onDelete, selected, onSelect }: Vers
minSize: 50,
maxSize: 50,
cell: props => (
<MiniButton
noHover
title='Удалить версию'
disabled={processing}
icon={<BiX size='1rem' className='icon-red' />}
onClick={() => onDelete(props.row.original.id)}
/>
<div className='h-[1.25rem] w-[1.25rem]'>
<MiniButton
title='Удалить версию'
noHover
noPadding
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}
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} />
</Overlay>
@ -182,14 +182,14 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton
noHover
title='Внести словоформу'
icon={<BiCheck size='1.25rem' className='icon-green' />}
icon={<BiCheck size='1.5rem' className='icon-green' />}
disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
onClick={handleAddForm}
/>
<MiniButton
noHover
title='Генерировать стандартные словоформы'
icon={<BiChevronsDown size='1.25rem' className='icon-primary' />}
icon={<BiChevronsDown size='1.5rem' className='icon-primary' />}
disabled={textProcessor.loading || !inputText}
onClick={handleGenerateLexeme}
/>
@ -200,7 +200,8 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<MiniButton
noHover
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}
onClick={handleResetAll}
/>

View File

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

View File

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

View File

@ -148,14 +148,21 @@ export interface ICstUpdateData
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;
substitution: ConstituentaID;
transfer_term: boolean;
}
/**
* Represents data, used in merging multiple {@link IConstituenta}.
*/
export interface ICstSubstituteData {
substitutions: ICstSubstitute[];
}
/**
* Represents single substitution for synthesis table.
*/
@ -251,5 +258,5 @@ export interface IInlineSynthesisData {
receiver: LibraryItemID;
source: LibraryItemID;
items: ConstituentaID[];
substitutions: ICstSubstituteData[];
substitutions: ICstSubstitute[];
}

View File

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

View File

@ -133,5 +133,6 @@ export const prefixes = {
topic_list: 'topic_list_',
library_list: 'library_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) {
case HelpTopic.MAIN: return 'Портал';
case HelpTopic.LIBRARY: return 'Библиотека';
case HelpTopic.RSFORM: return '- паспорт схемы';
case HelpTopic.RSFORM: return '- карточка схемы';
case HelpTopic.CSTLIST: return '- список конституент';
case HelpTopic.CONSTITUENTA: return '- конституента';
case HelpTopic.GRAPH_TERM: return '- граф термов';
case HelpTopic.RSTEMPLATES: return '- Банк выражений';
case HelpTopic.RSTEMPLATES: return '- шаблоны выражений';
case HelpTopic.RSLANG: return 'Экспликация';
case HelpTopic.TERM_CONTROL: return 'Терминологизация';
case HelpTopic.VERSIONS: return 'Версионирование';