F: Improve OSS <-> RSForm UI
Some checks are pending
Backend CI / build (3.12) (push) Waiting to run
Frontend CI / build (22.x) (push) Waiting to run

This commit is contained in:
Ivan 2024-08-01 00:36:06 +03:00
parent 4f9f27dfd3
commit 600b0c01ef
21 changed files with 185 additions and 59 deletions

View File

@ -5,6 +5,7 @@ from .data_access import (
LibraryItemBaseSerializer, LibraryItemBaseSerializer,
LibraryItemCloneSerializer, LibraryItemCloneSerializer,
LibraryItemDetailsSerializer, LibraryItemDetailsSerializer,
LibraryItemReferenceSerializer,
LibraryItemSerializer, LibraryItemSerializer,
UsersListSerializer, UsersListSerializer,
UserTargetSerializer, UserTargetSerializer,

View File

@ -17,6 +17,14 @@ class LibraryItemBaseSerializer(serializers.ModelSerializer):
read_only_fields = ('id',) read_only_fields = ('id',)
class LibraryItemReferenceSerializer(serializers.ModelSerializer):
''' Serializer: reference to LibraryItem. '''
class Meta:
''' serializer metadata. '''
model = LibraryItem
fields = 'id', 'alias'
class LibraryItemSerializer(serializers.ModelSerializer): class LibraryItemSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem entry limited access. ''' ''' Serializer: LibraryItem entry limited access. '''
class Meta: class Meta:

View File

@ -84,7 +84,7 @@ class OperationUpdateSerializer(serializers.Serializer):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
for operation in attrs['arguments']: for operation in attrs['arguments']:
if operation.oss != oss: if operation.oss_id != oss.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'arguments': msg.operationNotInOSS(oss.title) 'arguments': msg.operationNotInOSS(oss.title)
}) })
@ -110,7 +110,7 @@ class OperationUpdateSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{original_cst.pk}': msg.substituteDouble(original_cst.alias) f'{original_cst.pk}': msg.substituteDouble(original_cst.alias)
}) })
if original_cst.schema == substitution_cst.schema: if original_cst.schema_id == substitution_cst.schema_id:
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias) 'alias': msg.substituteTrivial(original_cst.alias)
}) })
@ -131,7 +131,7 @@ class OperationTargetSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target']) operation = cast(Operation, attrs['target'])
if oss and operation.oss != oss: if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title) 'target': msg.operationNotInOSS(oss.title)
}) })
@ -155,7 +155,7 @@ class SetOperationInputSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
operation = cast(Operation, attrs['target']) operation = cast(Operation, attrs['target'])
if oss and operation.oss != oss: if oss and operation.oss_id != oss.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'target': msg.operationNotInOSS(oss.title) 'target': msg.operationNotInOSS(oss.title)
}) })

View File

@ -4,11 +4,17 @@ from typing import Optional, cast
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.db.models import Q
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import PrimaryKeyRelatedField as PKField from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.library.models import LibraryItem from apps.library.models import LibraryItem
from apps.library.serializers import LibraryItemBaseSerializer, LibraryItemDetailsSerializer from apps.library.serializers import (
LibraryItemBaseSerializer,
LibraryItemDetailsSerializer,
LibraryItemReferenceSerializer
)
from apps.oss.models import Inheritance
from shared import messages as msg from shared import messages as msg
from ..models import Constituenta, CstType, RSForm from ..models import Constituenta, CstType, RSForm
@ -90,6 +96,12 @@ class RSFormSerializer(serializers.ModelSerializer):
items = serializers.ListField( items = serializers.ListField(
child=CstSerializer() child=CstSerializer()
) )
inheritance = serializers.ListField(
child=serializers.ListField(child=serializers.IntegerField())
)
oss = serializers.ListField(
child=LibraryItemReferenceSerializer()
)
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
@ -101,6 +113,15 @@ class RSFormSerializer(serializers.ModelSerializer):
result['items'] = [] result['items'] = []
for cst in RSForm(instance).constituents().order_by('order'): for cst in RSForm(instance).constituents().order_by('order'):
result['items'].append(CstSerializer(cst).data) result['items'].append(CstSerializer(cst).data)
result['inheritance'] = []
for link in Inheritance.objects.filter(Q(child__schema=instance) | Q(parent__schema=instance)):
result['inheritance'].append([link.child.pk, link.parent.pk])
result['oss'] = []
for oss in LibraryItem.objects.filter(items__result=instance).only('alias'):
result['oss'].append({
'id': oss.pk,
'alias': oss.alias
})
return result return result
def to_versioned_data(self) -> dict: def to_versioned_data(self) -> dict:
@ -109,6 +130,8 @@ class RSFormSerializer(serializers.ModelSerializer):
del result['versions'] del result['versions']
del result['subscribers'] del result['subscribers']
del result['editors'] del result['editors']
del result['inheritance']
del result['oss']
del result['owner'] del result['owner']
del result['visible'] del result['visible']
@ -210,7 +233,7 @@ class CstTargetSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
schema = cast(LibraryItem, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if schema and cst.schema != schema: if schema and cst.schema_id != schema.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.pk}': msg.constituentaNotInRSform(schema.title) f'{cst.pk}': msg.constituentaNotInRSform(schema.title)
}) })
@ -224,7 +247,7 @@ class CstTargetSerializer(serializers.Serializer):
class CstRenameSerializer(serializers.Serializer): class CstRenameSerializer(serializers.Serializer):
''' Serializer: Constituenta renaming. ''' ''' Serializer: Constituenta renaming. '''
target = PKField(many=False, queryset=Constituenta.objects.all()) target = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
alias = serializers.CharField() alias = serializers.CharField()
cst_type = serializers.CharField() cst_type = serializers.CharField()
@ -232,7 +255,7 @@ class CstRenameSerializer(serializers.Serializer):
attrs = super().validate(attrs) attrs = super().validate(attrs)
schema = cast(LibraryItem, self.context['schema']) schema = cast(LibraryItem, self.context['schema'])
cst = cast(Constituenta, attrs['target']) cst = cast(Constituenta, attrs['target'])
if cst.schema != schema: if cst.schema_id != schema.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.pk}': msg.constituentaNotInRSform(schema.title) f'{cst.pk}': msg.constituentaNotInRSform(schema.title)
}) })
@ -258,7 +281,7 @@ class CstListSerializer(serializers.Serializer):
return attrs return attrs
for item in attrs['items']: for item in attrs['items']:
if item.schema != schema: if item.schema_id != schema.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{item.pk}': msg.constituentaNotInRSform(schema.title) f'{item.pk}': msg.constituentaNotInRSform(schema.title)
}) })
@ -272,8 +295,8 @@ class CstMoveSerializer(CstListSerializer):
class SubstitutionSerializerBase(serializers.Serializer): class SubstitutionSerializerBase(serializers.Serializer):
''' Serializer: Basic substitution. ''' ''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.all()) original = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
substitution = PKField(many=False, queryset=Constituenta.objects.all()) substitution = PKField(many=False, queryset=Constituenta.objects.only('alias', 'schema'))
class CstSubstituteSerializer(serializers.Serializer): class CstSubstituteSerializer(serializers.Serializer):
@ -297,11 +320,11 @@ class CstSubstituteSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias) 'alias': msg.substituteTrivial(original_cst.alias)
}) })
if original_cst.schema != schema: if original_cst.schema_id != schema.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'original': msg.constituentaNotInRSform(schema.title) 'original': msg.constituentaNotInRSform(schema.title)
}) })
if substitution_cst.schema != schema: if substitution_cst.schema_id != schema.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'substitution': msg.constituentaNotInRSform(schema.title) 'substitution': msg.constituentaNotInRSform(schema.title)
}) })
@ -329,7 +352,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
}) })
constituents = cast(list[Constituenta], attrs['items']) constituents = cast(list[Constituenta], attrs['items'])
for cst in constituents: for cst in constituents:
if cst.schema != schema_in: if cst.schema_id != schema_in.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{cst.pk}': msg.constituentaNotInRSform(schema_in.title) f'{cst.pk}': msg.constituentaNotInRSform(schema_in.title)
}) })
@ -337,12 +360,12 @@ class InlineSynthesisSerializer(serializers.Serializer):
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'])
if original_cst.schema == schema_in: if original_cst.schema_id == schema_in.pk:
if original_cst not in constituents: if original_cst not in constituents:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{original_cst.pk}': msg.substitutionNotInList() f'{original_cst.pk}': msg.substitutionNotInList()
}) })
if substitution_cst.schema != schema_out: if substitution_cst.schema_id != schema_out.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{substitution_cst.pk}': msg.constituentaNotInRSform(schema_out.title) f'{substitution_cst.pk}': msg.constituentaNotInRSform(schema_out.title)
}) })
@ -351,7 +374,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{substitution_cst.pk}': msg.substitutionNotInList() f'{substitution_cst.pk}': msg.substitutionNotInList()
}) })
if original_cst.schema != schema_out: if original_cst.schema_id != schema_out.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
f'{original_cst.pk}': msg.constituentaNotInRSform(schema_out.title) f'{original_cst.pk}': msg.constituentaNotInRSform(schema_out.title)
}) })

View File

@ -102,6 +102,8 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved) self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved)
self.assertEqual(response.data['subscribers'], [self.user.pk]) self.assertEqual(response.data['subscribers'], [self.user.pk])
self.assertEqual(response.data['editors'], []) self.assertEqual(response.data['editors'], [])
self.assertEqual(response.data['inheritance'], [])
self.assertEqual(response.data['oss'], [])
self.executeOK(item=self.unowned_id) self.executeOK(item=self.unowned_id)
self.executeForbidden(item=self.private_id) self.executeForbidden(item=self.private_id)

View File

@ -62,6 +62,7 @@ export { VscLibrary as IconLibrary } from 'react-icons/vsc';
export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { GiHoneycomb as IconOSS } from 'react-icons/gi'; export { GiHoneycomb as IconOSS } from 'react-icons/gi';
export { LuBaby as IconChild } from 'react-icons/lu';
export { RiHexagonLine as IconRSForm } from 'react-icons/ri'; export { RiHexagonLine as IconRSForm } from 'react-icons/ri';
export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuArchive as IconArchive } from 'react-icons/lu';
export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu';

View File

@ -20,6 +20,7 @@ function BadgeConstituenta({ value, prefixID, theme }: BadgeConstituentaProps) {
'min-w-[3.1rem] max-w-[3.1rem]', // prettier: split lines 'min-w-[3.1rem] max-w-[3.1rem]', // prettier: split lines
'px-1', 'px-1',
'border rounded-md', 'border rounded-md',
value.is_inherited && 'border-dashed',
'text-center font-medium whitespace-nowrap' 'text-center font-medium whitespace-nowrap'
)} )}
style={{ style={{

View File

@ -13,7 +13,10 @@ interface InfoConstituentaProps extends CProps.Div {
function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) { function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) {
return ( return (
<div className={clsx('dense min-w-[15rem]', className)} {...restProps}> <div className={clsx('dense min-w-[15rem]', className)} {...restProps}>
<h2>Конституента {data.alias}</h2> <h2>
Конституента {data.alias}
{data.is_inherited ? ' (наследуется)' : ''}
</h2>
{data.term_resolved ? ( {data.term_resolved ? (
<p> <p>
<b>Термин: </b> <b>Термин: </b>

View File

@ -102,8 +102,6 @@ function PickSubstitutions({
}; };
const toDelete = substitutions.map(item => item.original); const toDelete = substitutions.map(item => item.original);
const replacements = substitutions.map(item => item.substitution); const replacements = substitutions.map(item => item.substitution);
console.log(toDelete, replacements);
console.log(newSubstitution);
if ( if (
toDelete.includes(newSubstitution.original) || toDelete.includes(newSubstitution.original) ||
toDelete.includes(newSubstitution.substitution) || toDelete.includes(newSubstitution.substitution) ||

View File

@ -59,6 +59,8 @@ export class RSFormLoader {
} }
private inferCstAttributes() { private inferCstAttributes() {
const inherit_children = new Set(this.schema.inheritance.map(item => item[0]));
const inherit_parents = new Set(this.schema.inheritance.map(item => item[1]));
this.graph.topologicalOrder().forEach(cstID => { this.graph.topologicalOrder().forEach(cstID => {
const cst = this.cstByID.get(cstID)!; const cst = this.cstByID.get(cstID)!;
cst.status = inferStatus(cst.parse.status, cst.parse.valueClass); cst.status = inferStatus(cst.parse.status, cst.parse.valueClass);
@ -66,6 +68,8 @@ export class RSFormLoader {
cst.cst_class = inferClass(cst.cst_type, cst.is_template); cst.cst_class = inferClass(cst.cst_type, cst.is_template);
cst.children = []; cst.children = [];
cst.children_alias = []; cst.children_alias = [];
cst.is_inherited = inherit_children.has(cst.id);
cst.is_inherited_parent = inherit_parents.has(cst.id);
cst.is_simple_expression = this.inferSimpleExpression(cst); cst.is_simple_expression = this.inferSimpleExpression(cst);
if (!cst.is_simple_expression || cst.cst_type === CstType.STRUCTURED) { if (!cst.is_simple_expression || cst.cst_type === CstType.STRUCTURED) {
return; return;
@ -165,6 +169,7 @@ export class RSFormLoader {
sum + (cst.parse.status === ParsingStatus.VERIFIED && cst.parse.valueClass === ValueClass.INVALID ? 1 : 0), sum + (cst.parse.status === ParsingStatus.VERIFIED && cst.parse.valueClass === ValueClass.INVALID ? 1 : 0),
0 0
), ),
count_inherited: items.reduce((sum, cst) => sum + ((cst as IConstituenta).is_inherited ? 1 : 0), 0),
count_text_term: items.reduce((sum, cst) => sum + (cst.term_raw ? 1 : 0), 0), count_text_term: items.reduce((sum, cst) => sum + (cst.term_raw ? 1 : 0), 0),
count_definition: items.reduce((sum, cst) => sum + (cst.definition_raw ? 1 : 0), 0), count_definition: items.reduce((sum, cst) => sum + (cst.definition_raw ? 1 : 0), 0),

View File

@ -75,7 +75,7 @@ export interface ILibraryItem {
} }
/** /**
* Represents library item constant data loaded for both OSS and RSForm. * Represents {@link ILibraryItem} constant data loaded for both OSS and RSForm.
*/ */
export interface ILibraryItemData extends ILibraryItem { export interface ILibraryItemData extends ILibraryItem {
subscribers: UserID[]; subscribers: UserID[];
@ -83,7 +83,12 @@ export interface ILibraryItemData extends ILibraryItem {
} }
/** /**
* Represents library item extended data with versions. * Represents {@link ILibraryItem} minimal reference data.
*/
export interface ILibraryItemReference extends Pick<ILibraryItem, 'id' | 'alias'> {}
/**
* Represents {@link ILibraryItem} extended data with versions.
*/ */
export interface ILibraryItemVersioned extends ILibraryItemData { export interface ILibraryItemVersioned extends ILibraryItemData {
version?: VersionID; version?: VersionID;
@ -91,7 +96,7 @@ export interface ILibraryItemVersioned extends ILibraryItemData {
} }
/** /**
* Represents common library item editor controller. * Represents common {@link ILibraryItem} editor controller.
*/ */
export interface ILibraryItemEditor { export interface ILibraryItemEditor {
schema?: ILibraryItemData; schema?: ILibraryItemData;

View File

@ -4,7 +4,7 @@
import { Graph } from '@/models/Graph'; import { Graph } from '@/models/Graph';
import { ILibraryItem, ILibraryItemVersioned, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemReference, ILibraryItemVersioned, LibraryItemID } from './library';
import { ICstSubstitute } from './oss'; import { ICstSubstitute } from './oss';
import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang'; import { IArgumentInfo, ParsingStatus, ValueClass } from './rslang';
@ -111,6 +111,8 @@ export interface IConstituenta extends IConstituentaData {
status: ExpressionStatus; status: ExpressionStatus;
is_template: boolean; is_template: boolean;
is_simple_expression: boolean; is_simple_expression: boolean;
is_inherited: boolean;
is_inherited_parent: boolean;
parent?: ConstituentaID; parent?: ConstituentaID;
parent_alias?: string; parent_alias?: string;
children: number[]; children: number[];
@ -183,6 +185,7 @@ export interface IRSFormStats {
count_errors: number; count_errors: number;
count_property: number; count_property: number;
count_incalculable: number; count_incalculable: number;
count_inherited: number;
count_text_term: number; count_text_term: number;
count_definition: number; count_definition: number;
@ -198,10 +201,19 @@ export interface IRSFormStats {
count_theorem: number; count_theorem: number;
} }
/**
* Represents data for {@link IRSForm} provided by backend.
*/
export interface IRSFormData extends ILibraryItemVersioned {
items: IConstituentaData[];
inheritance: ConstituentaID[][];
oss: ILibraryItemReference[];
}
/** /**
* Represents formal explication for set of concepts. * Represents formal explication for set of concepts.
*/ */
export interface IRSForm extends ILibraryItemVersioned { export interface IRSForm extends IRSFormData {
items: IConstituenta[]; items: IConstituenta[];
stats: IRSFormStats; stats: IRSFormStats;
graph: Graph; graph: Graph;
@ -209,13 +221,6 @@ export interface IRSForm extends ILibraryItemVersioned {
cstByID: Map<ConstituentaID, IConstituenta>; cstByID: Map<ConstituentaID, IConstituenta>;
} }
/**
* Represents data for {@link IRSForm} provided by backend.
*/
export interface IRSFormData extends ILibraryItemVersioned {
items: IConstituentaData[];
}
/** /**
* Represents data, used for cloning {@link IRSForm}. * Represents data, used for cloning {@link IRSForm}.
*/ */

View File

@ -133,6 +133,8 @@ export function createMockConstituenta(id: ConstituentaID, alias: string, commen
definition_resolved: '', definition_resolved: '',
status: ExpressionStatus.INCORRECT, status: ExpressionStatus.INCORRECT,
is_template: false, is_template: false,
is_inherited: false,
is_inherited_parent: false,
cst_class: CstClass.DERIVED, cst_class: CstClass.DERIVED,
parse: { parse: {
status: ParsingStatus.INCORRECT, status: ParsingStatus.INCORRECT,

View File

@ -21,7 +21,7 @@ function InputNode(node: OssNodeInternal) {
<> <>
<Handle type='source' position={Position.Bottom} /> <Handle type='source' position={Position.Bottom} />
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'> <Overlay position='top-[-0.2rem] right-[-0.2rem]'>
<MiniButton <MiniButton
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover

View File

@ -22,7 +22,7 @@ function OperationNode(node: OssNodeInternal) {
<> <>
<Handle type='source' position={Position.Bottom} /> <Handle type='source' position={Position.Bottom} />
<Overlay position='top-[-0.2rem] right-[-0.2rem]' className='cc-icons'> <Overlay position='top-[-0.2rem] right-[-0.2rem]'>
<MiniButton <MiniButton
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover

View File

@ -5,8 +5,10 @@ import { AnimatePresence } from 'framer-motion';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconSave } from '@/components/Icons'; import { IconChild, IconSave } from '@/components/Icons';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
@ -182,7 +184,7 @@ function FormConstituenta({
} }
value={expression} value={expression}
activeCst={state} activeCst={state}
disabled={disabled} disabled={disabled || state?.is_inherited}
toggleReset={toggleReset} toggleReset={toggleReset}
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
@ -229,15 +231,26 @@ function FormConstituenta({
Добавить комментарий Добавить комментарий
</button> </button>
) : null} ) : null}
{!disabled || processing ? ( {!disabled || processing ? (
<div className='self-center flex'>
<SubmitButton <SubmitButton
key='cst_form_submit' key='cst_form_submit'
id='cst_form_submit' id='cst_form_submit'
text='Сохранить изменения' text='Сохранить изменения'
className='self-center'
disabled={disabled || !isModified} disabled={disabled || !isModified}
icon={<IconSave size='1.25rem' />} icon={<IconSave size='1.25rem' />}
/> />
{state?.is_inherited ? (
<Overlay position='right-[-2rem]'>
<MiniButton
icon={<IconChild size='1.25rem' className='clr-text-red' />}
disabled
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
/>
</Overlay>
) : null}
</div>
) : null} ) : null}
</AnimatePresence> </AnimatePresence>
</form> </form>

View File

@ -1,19 +1,21 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useLayoutEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { IconSave } from '@/components/Icons'; import { IconOSS, IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion'; import SelectVersion from '@/components/select/SelectVersion';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useRSForm } from '@/context/RSFormContext'; import useDropdown from '@/hooks/useDropdown';
import { ILibraryUpdateData, LibraryItemType } from '@/models/library'; import { ILibraryUpdateData, LibraryItemType } from '@/models/library';
import { limits, patterns } from '@/utils/constants'; import { limits, patterns, prefixes } from '@/utils/constants';
import { information } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
import ToolbarItemAccess from './ToolbarItemAccess'; import ToolbarItemAccess from './ToolbarItemAccess';
@ -26,8 +28,8 @@ interface FormRSFormProps {
} }
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) { function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
const { schema, update, processing } = useRSForm();
const controller = useRSEdit(); const controller = useRSEdit();
const schema = controller.schema;
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -35,6 +37,8 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [readOnly, setReadOnly] = useState(false); const [readOnly, setReadOnly] = useState(false);
const ossMenu = useDropdown();
useEffect(() => { useEffect(() => {
if (!schema) { if (!schema) {
setIsModified(false); setIsModified(false);
@ -85,9 +89,37 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
visible: visible, visible: visible,
read_only: readOnly read_only: readOnly
}; };
update(data, () => toast.success(information.changesSaved)); controller.updateSchema(data);
}; };
const ossSelector = useMemo(
() =>
schema && schema?.oss.length > 0 ? (
<Overlay position='left-[12.5rem] top-[-0.4rem]'>
<div ref={ossMenu.ref}>
<MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />}
noHover
title='Связанные операционные схемы'
hideTitle={ossMenu.isOpen}
onClick={() => ossMenu.toggle()}
/>
<Dropdown isOpen={ossMenu.isOpen} className='mt-[-0.1rem]'>
{schema.oss.map((reference, index) => (
<DropdownButton
className='min-w-[5rem]'
key={`${prefixes.oss_list}${index}`}
text={reference.alias}
onClick={event => controller.viewOSS(reference.id, event.ctrlKey || event.metaKey)}
/>
))}
</Dropdown>
</div>
</Overlay>
) : null,
[schema, ossMenu, controller]
);
return ( return (
<form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}> <form id={id} className={clsx('mt-1 min-w-[22rem] sm:w-[30rem]', 'flex flex-col pt-1')} onSubmit={handleSubmit}>
<TextInput <TextInput
@ -99,6 +131,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable}
onChange={event => setTitle(event.target.value)} onChange={event => setTitle(event.target.value)}
/> />
{ossSelector}
<div className='flex justify-between w-full gap-3 mb-3'> <div className='flex justify-between w-full gap-3 mb-3'>
<TextInput <TextInput
id='schema_alias' id='schema_alias'
@ -143,7 +176,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center mt-4' className='self-center mt-4'
loading={processing} loading={controller.isProcessing}
disabled={!isModified} disabled={!isModified}
icon={<IconSave size='1.25rem' />} icon={<IconSave size='1.25rem' />}
/> />

View File

@ -15,12 +15,15 @@ function RSFormStats({ stats }: RSFormStatsProps) {
<Divider margins='my-2' className='sm:hidden' /> <Divider margins='my-2' className='sm:hidden' />
<LabeledValue id='count_all' label='Всего конституент' text={stats.count_all} /> <LabeledValue id='count_all' label='Всего конституент' text={stats.count_all} />
<LabeledValue id='count_errors' label='Некорректных' text={stats.count_errors} /> {stats.count_inherited !== 0 ? (
<LabeledValue id='count_inherited' label='Наследованные' text={stats.count_inherited} />
) : null}
<LabeledValue id='count_errors' label='Некорректные' text={stats.count_errors} />
{stats.count_property !== 0 ? ( {stats.count_property !== 0 ? (
<LabeledValue id='count_property' label='Неразмерных' text={stats.count_property} /> <LabeledValue id='count_property' label='Неразмерные' text={stats.count_property} />
) : null} ) : null}
{stats.count_incalculable !== 0 ? ( {stats.count_incalculable !== 0 ? (
<LabeledValue id='count_incalculable' label='Невычислимых' text={stats.count_incalculable} /> <LabeledValue id='count_incalculable' label='Невычислимые' text={stats.count_incalculable} />
) : null} ) : null}
<Divider margins='my-2' /> <Divider margins='my-2' />

View File

@ -24,7 +24,14 @@ import DlgInlineSynthesis from '@/dialogs/DlgInlineSynthesis';
import DlgRenameCst from '@/dialogs/DlgRenameCst'; import DlgRenameCst from '@/dialogs/DlgRenameCst';
import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst'; import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import { AccessPolicy, IVersionData, LocationHead, VersionID } from '@/models/library'; import {
AccessPolicy,
ILibraryUpdateData,
IVersionData,
LibraryItemID,
LocationHead,
VersionID
} from '@/models/library';
import { ICstSubstituteData } from '@/models/oss'; import { ICstSubstituteData } from '@/models/oss';
import { import {
ConstituentaID, ConstituentaID,
@ -56,6 +63,8 @@ export interface IRSEditContext {
canProduceStructure: boolean; canProduceStructure: boolean;
nothingSelected: boolean; nothingSelected: boolean;
updateSchema: (data: ILibraryUpdateData) => void;
setOwner: (newOwner: UserID) => void; setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void; setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void; promptEditors: () => void;
@ -68,6 +77,7 @@ export interface IRSEditContext {
toggleSelect: (target: ConstituentaID) => void; toggleSelect: (target: ConstituentaID) => void;
deselectAll: () => void; deselectAll: () => void;
viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
viewVersion: (version?: VersionID, newTab?: boolean) => void; viewVersion: (version?: VersionID, newTab?: boolean) => void;
createVersion: () => void; createVersion: () => void;
restoreVersion: () => void; restoreVersion: () => void;
@ -177,11 +187,21 @@ export const RSEditState = ({
[model.schema, setAccessLevel, model.isOwned, user, adminMode] [model.schema, setAccessLevel, model.isOwned, user, adminMode]
); );
const updateSchema = useCallback(
(data: ILibraryUpdateData) => model.update(data, () => toast.success(information.changesSaved)),
[model]
);
const viewVersion = useCallback( const viewVersion = useCallback(
(version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.itemID, version), newTab), (version?: VersionID, newTab?: boolean) => router.push(urls.schema(model.itemID, version), newTab),
[router, model] [router, model]
); );
const viewOSS = useCallback(
(target: LibraryItemID, newTab?: boolean) => router.push(urls.oss(target), newTab),
[router]
);
const createVersion = useCallback(() => { const createVersion = useCallback(() => {
if (isModified && !promptUnsaved()) { if (isModified && !promptUnsaved()) {
return; return;
@ -571,6 +591,7 @@ export const RSEditState = ({
<RSEditContext.Provider <RSEditContext.Provider
value={{ value={{
schema: model.schema, schema: model.schema,
updateSchema,
selected, selected,
isMutable, isMutable,
isContentEditable, isContentEditable,
@ -591,6 +612,7 @@ export const RSEditState = ({
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])), setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
deselectAll: () => setSelected([]), deselectAll: () => setSelected([]),
viewOSS,
viewVersion, viewVersion,
createVersion, createVersion,
restoreVersion, restoreVersion,

View File

@ -62,7 +62,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
); );
return ( return (
<div className='flex border-b clr-input overflow-hidden'> <div className='flex border-b clr-input'>
<SearchBar <SearchBar
id='constituents_search' id='constituents_search'
noBorder noBorder

View File

@ -139,6 +139,7 @@ export const globals = {
*/ */
export const prefixes = { export const prefixes = {
page_size: 'page_size_', page_size: 'page_size_',
oss_list: 'oss_list_',
cst_list: 'cst_list_', cst_list: 'cst_list_',
cst_inline_synth_list: 'cst_inline_synth_list_', cst_inline_synth_list: 'cst_inline_synth_list_',
cst_inline_synth_substitutes: 'cst_inline_synth_substitutes_', cst_inline_synth_substitutes: 'cst_inline_synth_substitutes_',