mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 21:10:38 +03:00
F: Improve OSS <-> RSForm UI
This commit is contained in:
parent
4f9f27dfd3
commit
600b0c01ef
|
@ -5,6 +5,7 @@ from .data_access import (
|
||||||
LibraryItemBaseSerializer,
|
LibraryItemBaseSerializer,
|
||||||
LibraryItemCloneSerializer,
|
LibraryItemCloneSerializer,
|
||||||
LibraryItemDetailsSerializer,
|
LibraryItemDetailsSerializer,
|
||||||
|
LibraryItemReferenceSerializer,
|
||||||
LibraryItemSerializer,
|
LibraryItemSerializer,
|
||||||
UsersListSerializer,
|
UsersListSerializer,
|
||||||
UserTargetSerializer,
|
UserTargetSerializer,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) ||
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ? (
|
||||||
<SubmitButton
|
<div className='self-center flex'>
|
||||||
key='cst_form_submit'
|
<SubmitButton
|
||||||
id='cst_form_submit'
|
key='cst_form_submit'
|
||||||
text='Сохранить изменения'
|
id='cst_form_submit'
|
||||||
className='self-center'
|
text='Сохранить изменения'
|
||||||
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>
|
||||||
|
|
|
@ -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' />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -14,13 +14,16 @@ function RSFormStats({ stats }: RSFormStatsProps) {
|
||||||
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'>
|
<div className='flex flex-col sm:gap-1 sm:ml-6 sm:mt-8 sm:w-[16rem]'>
|
||||||
<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' />
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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_',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user