Compare commits

...

4 Commits

Author SHA1 Message Date
Ivan
118c5459f3 F: Add Thesaurus manual page
Some checks are pending
Backend CI / build (3.12) (push) Waiting to run
Frontend CI / build (22.x) (push) Waiting to run
2024-08-21 12:37:27 +03:00
Ivan
3cb769a1d4 M: Small UI fixes 2024-08-20 18:26:17 +03:00
Ivan
0c8577e913 F: Remove subscriptions and notification icon 2024-08-20 15:09:36 +03:00
Ivan
c6b192b841 M: Minor UI fixes 2024-08-20 14:35:30 +03:00
43 changed files with 8180 additions and 8777 deletions

View File

@ -68,7 +68,6 @@ https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
- shadcn-ui
- Zod
- use-debounce
- react-query
- react-hook-form

View File

@ -28,15 +28,6 @@ class LibraryTemplateAdmin(admin.ModelAdmin):
return 'N/A'
class SubscriptionAdmin(admin.ModelAdmin):
''' Admin model: Subscriptions. '''
list_display = ['id', 'item', 'user']
search_fields = [
'item__title', 'item__alias',
'user__username', 'user__first_name', 'user__last_name'
]
class EditorAdmin(admin.ModelAdmin):
''' Admin model: Editors. '''
list_display = ['id', 'item', 'editor']
@ -57,6 +48,5 @@ class VersionAdmin(admin.ModelAdmin):
admin.site.register(models.LibraryItem, LibraryItemAdmin)
admin.site.register(models.LibraryTemplate, LibraryTemplateAdmin)
admin.site.register(models.Subscription, SubscriptionAdmin)
admin.site.register(models.Version, VersionAdmin)
admin.site.register(models.Editor, EditorAdmin)

View File

@ -0,0 +1,16 @@
# Generated by Django 5.1 on 2024-08-20 11:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0003_alter_librarytemplate_lib_source'),
]
operations = [
migrations.DeleteModel(
name='Subscription',
),
]

View File

@ -112,10 +112,6 @@ class LibraryItem(Model):
def get_absolute_url(self):
return f'/api/library/{self.pk}'
def subscribers(self) -> QuerySet[User]:
''' Get all subscribers for this item. '''
return User.objects.filter(subscription__item=self.pk)
def editors(self) -> QuerySet[User]:
''' Get all Editors of this item. '''
return User.objects.filter(editor__item=self.pk)

View File

@ -1,44 +0,0 @@
''' Models: Subscription. '''
from django.db.models import CASCADE, ForeignKey, Model
from apps.users.models import User
class Subscription(Model):
''' User subscription to library item. '''
user: ForeignKey = ForeignKey(
verbose_name='Пользователь',
to=User,
on_delete=CASCADE
)
item: ForeignKey = ForeignKey(
verbose_name='Элемент',
to='library.LibraryItem',
on_delete=CASCADE
)
class Meta:
''' Model metadata. '''
verbose_name = 'Подписка'
verbose_name_plural = 'Подписки'
unique_together = [['user', 'item']]
def __str__(self) -> str:
return f'{self.user} -> {self.item}'
@staticmethod
def subscribe(user: int, item: int) -> bool:
''' Add subscription. '''
if Subscription.objects.filter(user_id=user, item_id=item).exists():
return False
Subscription.objects.create(user_id=user, item_id=item)
return True
@staticmethod
def unsubscribe(user: int, item: int) -> bool:
''' Remove subscription. '''
sub = Subscription.objects.filter(user_id=user, item_id=item).only('pk')
if not sub.exists():
return False
sub.delete()
return True

View File

@ -3,5 +3,4 @@
from .Editor import Editor
from .LibraryItem import AccessPolicy, LibraryItem, LibraryItemType, LocationHead, validate_location
from .LibraryTemplate import LibraryTemplate
from .Subscription import Subscription
from .Version import Version

View File

@ -72,7 +72,6 @@ class VersionCreateSerializer(serializers.ModelSerializer):
class LibraryItemDetailsSerializer(serializers.ModelSerializer):
''' Serializer: LibraryItem detailed data. '''
subscribers = serializers.SerializerMethodField()
editors = serializers.SerializerMethodField()
versions = serializers.SerializerMethodField()
@ -82,9 +81,6 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
fields = '__all__'
read_only_fields = ('owner', 'id', 'item_type')
def get_subscribers(self, instance: LibraryItem) -> list[int]:
return list(instance.subscribers().values_list('pk', flat=True))
def get_editors(self, instance: LibraryItem) -> list[int]:
return list(instance.editors().values_list('pk', flat=True))

View File

@ -1,4 +1,3 @@
''' Tests for Django Models. '''
from .t_Editor import *
from .t_LibraryItem import *
from .t_Subscription import *

View File

@ -6,7 +6,6 @@ from apps.library.models import (
LibraryItem,
LibraryItemType,
LocationHead,
Subscription,
validate_location
)
from apps.users.models import User

View File

@ -1,67 +0,0 @@
''' Testing models: Subscription. '''
from django.test import TestCase
from apps.library.models import LibraryItem, LibraryItemType, Subscription
from apps.users.models import User
class TestSubscription(TestCase):
''' Testing Subscription model. '''
def setUp(self):
self.user1 = User.objects.create(username='User1')
self.user2 = User.objects.create(username='User2')
self.item = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
title='Test',
alias='КС1',
owner=self.user1
)
def test_default(self):
subs = list(Subscription.objects.filter(item=self.item))
self.assertEqual(len(subs), 0)
def test_str(self):
testStr = 'User2 -> КС1'
item = Subscription.objects.create(
user=self.user2,
item=self.item
)
self.assertEqual(str(item), testStr)
def test_subscribe(self):
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
self.assertEqual(item.subscribers().count(), 0)
self.assertTrue(Subscription.subscribe(self.user1.pk, item.pk))
self.assertEqual(item.subscribers().count(), 1)
self.assertTrue(self.user1 in item.subscribers())
self.assertFalse(Subscription.subscribe(self.user1.pk, item.pk))
self.assertEqual(item.subscribers().count(), 1)
self.assertTrue(Subscription.subscribe(self.user2.pk, item.pk))
self.assertEqual(item.subscribers().count(), 2)
self.assertTrue(self.user1 in item.subscribers())
self.assertTrue(self.user2 in item.subscribers())
self.user1.delete()
self.assertEqual(item.subscribers().count(), 1)
def test_unsubscribe(self):
item = LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='Test')
self.assertFalse(Subscription.unsubscribe(self.user1.pk, item.pk))
Subscription.subscribe(self.user1.pk, item.pk)
Subscription.subscribe(self.user2.pk, item.pk)
self.assertEqual(item.subscribers().count(), 2)
self.assertTrue(Subscription.unsubscribe(self.user1.pk, item.pk))
self.assertEqual(item.subscribers().count(), 1)
self.assertTrue(self.user2 in item.subscribers())
self.assertFalse(Subscription.unsubscribe(self.user1.pk, item.pk))

View File

@ -7,8 +7,7 @@ from apps.library.models import (
LibraryItem,
LibraryItemType,
LibraryTemplate,
LocationHead,
Subscription
LocationHead
)
from apps.rsform.models import RSForm
from shared.EndpointTester import EndpointTester, decl_endpoint
@ -49,7 +48,6 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.data['item_type'], LibraryItemType.RSFORM)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(response.data['alias'], data['alias'])
self.assertTrue(Subscription.objects.filter(user=self.user, item_id=response.data['id']).exists())
data = {
'item_type': LibraryItemType.OPERATION_SCHEMA,
@ -261,38 +259,6 @@ class TestLibraryViewset(EndpointTester):
self.executeForbidden()
@decl_endpoint('/api/library/active', method='get')
def test_retrieve_subscribed(self):
response = self.executeOK()
self.assertFalse(response_contains(response, self.unowned))
Subscription.subscribe(user=self.user.pk, item=self.unowned.pk)
Subscription.subscribe(user=self.user2.pk, item=self.unowned.pk)
Subscription.subscribe(user=self.user2.pk, item=self.owned.pk)
response = self.executeOK()
self.assertTrue(response_contains(response, self.unowned))
self.assertEqual(len(response.data), 3)
@decl_endpoint('/api/library/{item}/subscribe', method='post')
def test_subscriptions(self):
self.executeNotFound(item=self.invalid_item)
response = self.client.delete(f'/api/library/{self.unowned.pk}/unsubscribe')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(self.user in self.unowned.subscribers())
response = self.executeOK(item=self.unowned.pk)
self.assertTrue(self.user in self.unowned.subscribers())
response = self.executeOK(item=self.unowned.pk)
self.assertTrue(self.user in self.unowned.subscribers())
response = self.client.delete(f'/api/library/{self.unowned.pk}/unsubscribe')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(self.user in self.unowned.subscribers())
@decl_endpoint('/api/library/templates', method='get')
def test_retrieve_templates(self):
response = self.executeOK()

View File

@ -39,11 +39,9 @@ class LibraryViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer) -> None:
if not self.request.user.is_anonymous and 'owner' not in self.request.POST:
instance = serializer.save(owner=self.request.user)
serializer.save(owner=self.request.user)
else:
instance = serializer.save()
if instance.owner:
m.Subscription.subscribe(user=instance.owner_id, item=instance.pk)
serializer.save()
def perform_update(self, serializer) -> None:
instance = serializer.save()
@ -84,9 +82,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
access_level = permissions.ItemOwner
elif self.action in [
'create',
'clone',
'subscribe',
'unsubscribe'
'clone'
]:
access_level = permissions.GlobalUser
else:
@ -140,40 +136,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
data=RSFormParseSerializer(clone).data
)
@extend_schema(
summary='subscribe to item',
tags=['Library'],
request=None,
responses={
c.HTTP_200_OK: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['post'])
def subscribe(self, request: Request, pk):
''' Endpoint: Subscribe current user to item. '''
item = self._get_item()
m.Subscription.subscribe(user=cast(int, self.request.user.pk), item=item.pk)
return Response(status=c.HTTP_200_OK)
@extend_schema(
summary='unsubscribe from item',
tags=['Library'],
request=None,
responses={
c.HTTP_200_OK: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
},
)
@action(detail=True, methods=['delete'])
def unsubscribe(self, request: Request, pk) -> HttpResponse:
''' Endpoint: Unsubscribe current user from item. '''
item = self._get_item()
m.Subscription.unsubscribe(user=cast(int, self.request.user.pk), item=item.pk)
return Response(status=c.HTTP_200_OK)
@extend_schema(
summary='set owner for item',
tags=['Library'],
@ -336,8 +298,7 @@ class LibraryActiveView(generics.ListAPIView):
return m.LibraryItem.objects.filter(
(is_public & common_location) |
Q(owner=user) |
Q(editor__editor=user) |
Q(subscription__user=user)
Q(editor__editor=user)
).distinct().order_by('-time_update')

View File

@ -96,9 +96,6 @@ class CstCreateSerializer(serializers.ModelSerializer):
class RSFormSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for RSForm. '''
subscribers = serializers.ListField(
child=serializers.IntegerField()
)
editors = serializers.ListField(
child=serializers.IntegerField()
)
@ -137,7 +134,6 @@ class RSFormSerializer(serializers.ModelSerializer):
''' Create serializable version representation without redundant data. '''
result = self.to_representation(cast(LibraryItem, self.instance))
del result['versions']
del result['subscribers']
del result['editors']
del result['inheritance']
del result['oss']
@ -199,9 +195,6 @@ class RSFormSerializer(serializers.ModelSerializer):
class RSFormParseSerializer(serializers.ModelSerializer):
''' Serializer: Detailed data for RSForm including parse. '''
subscribers = serializers.ListField(
child=serializers.IntegerField()
)
editors = serializers.ListField(
child=serializers.IntegerField()
)

View File

@ -100,7 +100,6 @@ class TestRSFormViewset(EndpointTester):
self.assertEqual(response.data['items'][1]['id'], x2.pk)
self.assertEqual(response.data['items'][1]['term_raw'], x2.term_raw)
self.assertEqual(response.data['items'][1]['term_resolved'], x2.term_resolved)
self.assertEqual(response.data['subscribers'], [])
self.assertEqual(response.data['editors'], [])
self.assertEqual(response.data['inheritance'], [])
self.assertEqual(response.data['oss'], [])

View File

@ -3,7 +3,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
from apps.library.models import Editor, Subscription
from apps.library.models import Editor
from shared import messages as msg
from . import models
@ -59,9 +59,6 @@ class AuthSerializer(serializers.Serializer):
id = serializers.IntegerField()
username = serializers.CharField()
is_staff = serializers.BooleanField()
subscriptions = serializers.ListField(
child=serializers.IntegerField()
)
def to_representation(self, instance: models.User) -> dict:
if instance.is_anonymous:
@ -69,7 +66,6 @@ class AuthSerializer(serializers.Serializer):
'id': None,
'username': '',
'is_staff': False,
'subscriptions': [],
'editor': []
}
else:
@ -77,7 +73,6 @@ class AuthSerializer(serializers.Serializer):
'id': instance.pk,
'username': instance.username,
'is_staff': instance.is_staff,
'subscriptions': [sub.item.pk for sub in Subscription.objects.filter(user=instance)],
'editor': [edit.item.pk for edit in Editor.objects.filter(editor=instance)]
}

View File

@ -43,7 +43,6 @@ class TestUserAPIViews(EndpointTester):
self.assertEqual(response.data['id'], self.user.pk)
self.assertEqual(response.data['username'], self.user.username)
self.assertEqual(response.data['is_staff'], self.user.is_staff)
self.assertEqual(response.data['subscriptions'], [])
self.assertEqual(response.data['editor'], [])
self.logout()
@ -51,7 +50,6 @@ class TestUserAPIViews(EndpointTester):
self.assertEqual(response.data['id'], None)
self.assertEqual(response.data['username'], '')
self.assertEqual(response.data['is_staff'], False)
self.assertEqual(response.data['subscriptions'], [])
self.assertEqual(response.data['editor'], [])

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@ from rest_framework.permissions import \
from rest_framework.request import Request
from rest_framework.views import APIView
from apps.library.models import AccessPolicy, Editor, LibraryItem, Subscription, Version
from apps.library.models import AccessPolicy, Editor, LibraryItem, Version
from apps.oss.models import Operation
from apps.rsform.models import Constituenta
from apps.users.models import User
@ -24,7 +24,7 @@ def _extract_item(obj: Any) -> LibraryItem:
return cast(LibraryItem, obj.schema)
elif isinstance(obj, Operation):
return cast(LibraryItem, obj.oss)
elif isinstance(obj, (Version, Subscription, Editor)):
elif isinstance(obj, (Version, Editor)):
return cast(LibraryItem, obj.item)
raise PermissionDenied({
'message': 'Invalid type error. Please contact developers',

View File

@ -101,20 +101,6 @@ export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>
});
}
export function postSubscribe(target: string, request: FrontAction) {
AxiosPost({
endpoint: `/api/library/${target}/subscribe`,
request: request
});
}
export function deleteUnsubscribe(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}/unsubscribe`,
request: request
});
}
export function postCreateVersion(target: string, request: FrontExchange<IVersionData, IVersionCreatedResponse>) {
AxiosPost({
endpoint: `/api/library/${target}/create-version`,

View File

@ -6,8 +6,6 @@ import {
IconAlias,
IconBusiness,
IconFilter,
IconFollow,
IconFollowOff,
IconFormula,
IconGraphCollapse,
IconGraphExpand,
@ -64,14 +62,6 @@ export function VisibilityIcon({ value, size = '1.25rem', className }: DomIconPr
}
}
export function SubscribeIcon({ value, size = '1.25rem', className }: DomIconProps<boolean>) {
if (value) {
return <IconFollow size={size} className={className ?? 'clr-text-green'} />;
} else {
return <IconFollowOff size={size} className={className ?? 'clr-text-red'} />;
}
}
export function LocationIcon({ value, size = '1.25rem', className }: DomIconProps<string>) {
switch (value.substring(0, 2) as LocationHead) {
case LocationHead.COMMON:

View File

@ -100,9 +100,7 @@ export { BiUpvote as IconMoveUp } from 'react-icons/bi';
export { BiDownvote as IconMoveDown } from 'react-icons/bi';
export { BiRightArrow as IconMoveRight } from 'react-icons/bi';
export { BiLeftArrow as IconMoveLeft } from 'react-icons/bi';
export { FiBell as IconFollow } from 'react-icons/fi';
export { TbHexagonPlus2 as IconNewRSForm } from 'react-icons/tb';
export { FiBellOff as IconFollowOff } from 'react-icons/fi';
export { BiPlusCircle as IconNewItem } from 'react-icons/bi';
export { FaSquarePlus as IconNewItem2 } from 'react-icons/fa6';
export { BiDuplicate as IconClone } from 'react-icons/bi';

View File

@ -103,9 +103,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
if (filter.isOwned !== undefined) {
result = result.filter(item => filter.isOwned === (item.owner === user?.id));
}
if (filter.isSubscribed !== undefined) {
result = result.filter(item => filter.isSubscribed == user?.subscriptions.includes(item.id));
}
if (filter.isEditor !== undefined) {
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
}
@ -213,9 +210,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
(data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => {
const onSuccess = (newSchema: ILibraryItem) =>
reloadItems(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
});
setProcessingError(undefined);
@ -249,12 +243,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
onError: setProcessingError,
onSuccess: () =>
reloadItems(() => {
if (user?.subscriptions.includes(target)) {
user.subscriptions.splice(
user.subscriptions.findIndex(item => item === target),
1
);
}
if (callback) callback();
})
});
@ -275,9 +263,6 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
onError: setProcessingError,
onSuccess: newSchema =>
reloadItems(() => {
if (user && !user.subscriptions.includes(newSchema.id)) {
user.subscriptions.push(newSchema.id);
}
if (callback) callback(newSchema);
})
});

View File

@ -4,13 +4,11 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
import { DataCallback } from '@/backend/apiTransport';
import {
deleteUnsubscribe,
patchLibraryItem,
patchSetAccessPolicy,
patchSetEditors,
patchSetLocation,
patchSetOwner,
postSubscribe
patchSetOwner
} from '@/backend/library';
import {
patchCreateInput,
@ -52,12 +50,9 @@ interface IOssContext {
processingError: ErrorData;
isOwned: boolean;
isSubscribed: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
subscribe: (callback?: () => void) => void;
unsubscribe: (callback?: () => void) => void;
setOwner: (newOwner: UserID, callback?: () => void) => void;
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void;
@ -94,19 +89,10 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo(() => {
return user?.id === model?.owner || false;
}, [user, model?.owner]);
const isSubscribed = useMemo(() => {
if (!user || !model || !user.id) {
return false;
}
return model.subscribers.includes(user.id);
}, [user, model, toggleTracking]);
useEffect(() => {
oss.setID(itemID);
}, [itemID, oss.setID]);
@ -133,56 +119,6 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, model, library.localUpdateItem, oss.setData]
);
const subscribe = useCallback(
(callback?: () => void) => {
if (!model || !user) {
return;
}
setProcessingError(undefined);
postSubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && !model.subscribers.includes(user.id)) {
model.subscribers.push(user.id);
}
if (!user.subscriptions.includes(model.id)) {
user.subscriptions.push(model.id);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, user, model]
);
const unsubscribe = useCallback(
(callback?: () => void) => {
if (!model || !user) {
return;
}
setProcessingError(undefined);
deleteUnsubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && model.subscribers.includes(user.id)) {
model.subscribers.splice(model.subscribers.indexOf(user.id), 1);
}
if (user.subscriptions.includes(model.id)) {
user.subscriptions.splice(user.subscriptions.indexOf(model.id), 1);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, model, user]
);
const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => {
if (!model) {
@ -421,11 +357,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
processing,
processingError,
isOwned,
isSubscribed,
update,
subscribe,
unsubscribe,
setOwner,
setEditors,
setAccessPolicy,

View File

@ -4,14 +4,12 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react
import { DataCallback } from '@/backend/apiTransport';
import {
deleteUnsubscribe,
patchLibraryItem,
patchSetAccessPolicy,
patchSetEditors,
patchSetLocation,
patchSetOwner,
postCreateVersion,
postSubscribe
postCreateVersion
} from '@/backend/library';
import { postFindPredecessor } from '@/backend/oss';
import {
@ -68,14 +66,11 @@ interface IRSFormContext {
isArchive: boolean;
isOwned: boolean;
isSubscribed: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
download: (callback: DataCallback<Blob>) => void;
upload: (data: IRSFormUploadData, callback: () => void) => void;
subscribe: (callback?: () => void) => void;
unsubscribe: (callback?: () => void) => void;
setOwner: (newOwner: UserID, callback?: () => void) => void;
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void;
@ -132,21 +127,12 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [toggleTracking, setToggleTracking] = useState(false);
const isOwned = useMemo(() => {
return user?.id === schema?.owner || false;
}, [user, schema?.owner]);
const isArchive = useMemo(() => !!versionID, [versionID]);
const isSubscribed = useMemo(() => {
if (!user || !schema || !user.id) {
return false;
}
return schema.subscribers.includes(user.id);
}, [user, schema, toggleTracking]);
const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!schema) {
@ -190,56 +176,6 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
[itemID, setSchema, schema, library.localUpdateItem]
);
const subscribe = useCallback(
(callback?: () => void) => {
if (!schema || !user) {
return;
}
setProcessingError(undefined);
postSubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && !schema.subscribers.includes(user.id)) {
schema.subscribers.push(user.id);
}
if (!user.subscriptions.includes(schema.id)) {
user.subscriptions.push(schema.id);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, schema, user]
);
const unsubscribe = useCallback(
(callback?: () => void) => {
if (!schema || !user) {
return;
}
setProcessingError(undefined);
deleteUnsubscribe(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
if (user.id && schema.subscribers.includes(user.id)) {
schema.subscribers.splice(schema.subscribers.indexOf(user.id), 1);
}
if (user.subscriptions.includes(schema.id)) {
user.subscriptions.splice(user.subscriptions.indexOf(schema.id), 1);
}
setToggleTracking(prev => !prev);
if (callback) callback();
}
});
},
[itemID, schema, user]
);
const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => {
if (!schema) {
@ -635,7 +571,6 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
processing,
processingError,
isOwned,
isSubscribed,
isArchive,
update,
download,
@ -644,9 +579,6 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) =
resetAliases,
produceStructure,
inlineSynthesis,
subscribe,
unsubscribe,
setOwner,
setEditors,
setAccessPolicy,

View File

@ -78,7 +78,6 @@ export interface ILibraryItem {
* Represents {@link ILibraryItem} constant data loaded for both OSS and RSForm.
*/
export interface ILibraryItemData extends ILibraryItem {
subscribers: UserID[];
editors: UserID[];
}
@ -109,7 +108,6 @@ export interface ILibraryItemEditor {
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
toggleSubscribe: () => void;
share: () => void;
}

View File

@ -66,6 +66,8 @@ export type FontStyle = 'controls' | 'main' | 'math' | 'math2';
export enum HelpTopic {
MAIN = 'main',
THESAURUS = 'thesaurus',
INTERFACE = 'user-interface',
UI_LIBRARY = 'ui-library',
UI_RS_MENU = 'ui-rsform-menu',
@ -112,6 +114,8 @@ export enum HelpTopic {
export const topicParent = new Map<HelpTopic, HelpTopic>([
[HelpTopic.MAIN, HelpTopic.MAIN],
[HelpTopic.THESAURUS, HelpTopic.THESAURUS],
[HelpTopic.INTERFACE, HelpTopic.INTERFACE],
[HelpTopic.UI_LIBRARY, HelpTopic.INTERFACE],
[HelpTopic.UI_RS_MENU, HelpTopic.INTERFACE],
@ -182,7 +186,6 @@ export interface ILibraryFilter {
isVisible?: boolean;
isOwned?: boolean;
isSubscribed?: boolean;
isEditor?: boolean;
}

View File

@ -57,7 +57,6 @@ function LibraryPage() {
filter.head !== undefined ||
filter.isEditor !== undefined ||
filter.isOwned !== undefined ||
filter.isSubscribed !== undefined ||
filter.isVisible !== true ||
!!filter.location,
[filter]
@ -69,7 +68,6 @@ function LibraryPage() {
const toggleVisible = useCallback(() => setIsVisible(prev => toggleTristateFlag(prev)), [setIsVisible]);
const toggleOwned = useCallback(() => setIsOwned(prev => toggleTristateFlag(prev)), [setIsOwned]);
const toggleSubscribed = useCallback(() => setIsSubscribed(prev => toggleTristateFlag(prev)), [setIsSubscribed]);
const toggleEditor = useCallback(() => setIsEditor(prev => toggleTristateFlag(prev)), [setIsEditor]);
const toggleFolderMode = useCallback(() => setFolderMode(prev => !prev), [setFolderMode]);
@ -129,8 +127,6 @@ function LibraryPage() {
isOwned={isOwned}
toggleOwned={toggleOwned}
toggleVisible={toggleVisible}
isSubscribed={isSubscribed}
toggleSubscribed={toggleSubscribed}
isEditor={isEditor}
toggleEditor={toggleEditor}
resetFilter={resetFilter}

View File

@ -3,7 +3,7 @@
import clsx from 'clsx';
import { useCallback } from 'react';
import { LocationIcon, SubscribeIcon, VisibilityIcon } from '@/components/DomainIcons';
import { LocationIcon, VisibilityIcon } from '@/components/DomainIcons';
import { IconEditor, IconFilterReset, IconFolder, IconFolderTree, IconOwner } from '@/components/Icons';
import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown';
@ -37,8 +37,6 @@ interface ToolbarSearchProps {
toggleVisible: () => void;
isOwned: boolean | undefined;
toggleOwned: () => void;
isSubscribed: boolean | undefined;
toggleSubscribed: () => void;
isEditor: boolean | undefined;
toggleEditor: () => void;
resetFilter: () => void;
@ -63,8 +61,6 @@ function ToolbarSearch({
toggleVisible,
isOwned,
toggleOwned,
isSubscribed,
toggleSubscribed,
isEditor,
toggleEditor,
resetFilter
@ -118,11 +114,6 @@ function ToolbarSearch({
icon={<VisibilityIcon value={true} className={tripleToggleColor(isVisible)} />}
onClick={toggleVisible}
/>
<MiniButton
title='Я - Подписчик'
icon={<SubscribeIcon value={true} className={tripleToggleColor(isSubscribed)} />}
onClick={toggleSubscribed}
/>
<MiniButton
title='Я - Владелец'

View File

@ -15,6 +15,7 @@ import HelpInterface from './items/HelpInterface';
import HelpMain from './items/HelpMain';
import HelpRSLang from './items/HelpRSLang';
import HelpTerminologyControl from './items/HelpTerminologyControl';
import HelpThesaurus from './items/HelpThesaurus';
import HelpVersions from './items/HelpVersions';
import HelpAPI from './items/info/HelpAPI';
import HelpContributors from './items/info/HelpContributors';
@ -51,6 +52,7 @@ function TopicPage({ topic }: TopicPageProps) {
const size = useWindowSize();
if (topic === HelpTopic.MAIN) return <HelpMain />;
if (topic === HelpTopic.THESAURUS) return <HelpThesaurus />;
if (topic === HelpTopic.INTERFACE) return <HelpInterface />;
if (topic === HelpTopic.UI_LIBRARY) return <HelpLibrary />;

View File

@ -0,0 +1,44 @@
import { IconRSForm } from '@/components/Icons';
import LinkTopic from '@/components/ui/LinkTopic';
import { HelpTopic } from '@/models/miscellaneous';
function HelpThesaurus() {
return (
<div className='text-justify'>
<h1>Тезаурус</h1>
<p>
Данные раздел содержит основные термины и определения, используемые в работе с Порталом. Термины сгруппированы
по ключевым сущностям. Более подробно описание отношений между терминами даются в отдельных разделах данной
Справки через гиперссылки. Также указываются графические обозначения (иконки, цвета), используемые для
обозначения соответствующих сущностей в интерфейсе Портала.
</p>
<h2>Концептуализация</h2>
<p>Раздел в разработке...</p>
<h2>Концептуальная схема</h2>
<p>
<IconRSForm size='1rem' className='inline-icon' />{' '}
<LinkTopic text='Концептуальная схема' topic={HelpTopic.CC_SYSTEM} /> (система определений, КС) совокупность
отдельных понятий и утверждений, а также связей между ними, задаваемых определениями.
</p>
<p>
Экспликация КС изложение (процесс и результат) концептуальной схемы с помощью заданного языка описания
набора формальных конструкций и правил построения определений.
</p>
<p>
Родоструктурная экспликация КС экспликация КС с помощью{' '}
<LinkTopic text='аппарата родов структур' topic={HelpTopic.RSLANG} />.
</p>
<h2>Конституента</h2>
<p>Раздел в разработке...</p>
<h2>Операционная схема синтеза</h2>
<p>Раздел в разработке...</p>
<h2>Операция</h2>
<p>Раздел в разработке...</p>
</div>
);
}
export default HelpThesaurus;

View File

@ -3,7 +3,6 @@ import {
IconDestroy,
IconDownload,
IconEditor,
IconFollow,
IconImmutable,
IconOSS,
IconOwner,
@ -20,7 +19,7 @@ function HelpRSCard() {
<p>Карточка содержит общую информацию и статистику</p>
<p>
Карточка позволяет управлять атрибутами схемы и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
Карточка позволяет управлять атрибутами и <LinkTopic text='версиями' topic={HelpTopic.VERSIONS} />
</p>
<p>
Карточка позволяет назначать <IconEditor className='inline-icon' /> Редакторов
@ -46,14 +45,11 @@ function HelpRSCard() {
<IconPublic className='inline-icon' /> Общедоступные схемы видны всем посетителям
</li>
<li>
<IconImmutable className='inline-icon' /> Неизменные схемы редактируют только администраторы
<IconImmutable className='inline-icon' /> Неизменные схемы
</li>
<li>
<IconClone className='inline-icon icon-green' /> Клонировать создать копию схемы
</li>
<li>
<IconFollow className='inline-icon' /> Отслеживание схема в персональном списке
</li>
<li>
<IconDownload className='inline-icon' /> Загрузить/Выгрузить взаимодействие с Экстеор
</li>

View File

@ -4,7 +4,6 @@ import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext';
import { useOSS } from '@/context/OssContext';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
@ -21,8 +20,7 @@ interface EditorOssCardProps {
}
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
const { schema, isSubscribed } = useOSS();
const { user } = useAuth();
const { schema } = useOSS();
const controller = useOssEdit();
function initiateSubmit() {
@ -44,9 +42,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
return (
<>
<ToolbarRSFormCard
subscribed={isSubscribed}
modified={isModified}
anonymous={!user}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
controller={controller}

View File

@ -22,29 +22,24 @@ function NodeCore({ node }: NodeCoreProps) {
const longLabel = node.data.label.length > PARAMETER.ossLongLabel;
const labelText = truncateToLastWord(node.data.label, PARAMETER.ossTruncateLabel);
const handleOpenSchema = () => {
controller.openOperationSchema(Number(node.id));
};
return (
<>
<Overlay position='top-0 right-0' className='flex flex-col gap-1 p-[2px]'>
<MiniButton
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='12px' />}
disabled
noHover
noPadding
title={hasFile ? 'Связанная КС' : 'Нет связанной КС'}
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='12px' />}
hideTitle={!controller.showTooltip}
onClick={handleOpenSchema}
disabled={!hasFile}
/>
{node.data.operation.is_consolidation ? (
<MiniButton
icon={<IconConsolidation className='clr-text-primary' size='12px' />}
disabled
noPadding
noHover
titleHtml='<b>Внимание!</b><br />Ромбовидный синтез</br/>Возможны дубликаты конституент'
icon={<IconConsolidation className='clr-text-primary' size='12px' />}
hideTitle={!controller.showTooltip}
/>
) : null}

View File

@ -239,6 +239,9 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
}, []);
const handleSaveImage = useCallback(() => {
if (!model.schema) {
return;
}
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
if (canvas === null) {
toast.error(errors.imageFailed);
@ -261,7 +264,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
})
.then(dataURL => {
const a = document.createElement('a');
a.setAttribute('download', 'reactflow.svg');
a.setAttribute('download', `${model.schema?.alias ?? 'oss'}.svg`);
a.setAttribute('href', dataURL);
a.click();
})
@ -269,7 +272,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
console.error(error);
toast.error(errors.imageFailed);
});
}, [colors, nodes]);
}, [colors, nodes, model.schema]);
const handleContextMenu = useCallback(
(event: CProps.EventMouse, node: OssNode) => {

View File

@ -58,7 +58,6 @@ export interface IOssEditContext extends ILibraryItemEditor {
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
toggleSubscribe: () => void;
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
@ -173,14 +172,6 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
.catch(console.error);
}, []);
const toggleSubscribe = useCallback(() => {
if (model.isSubscribed) {
model.unsubscribe(() => toast.success(information.unsubscribed));
} else {
model.subscribe(() => toast.success(information.subscribed));
}
}, [model]);
const setOwner = useCallback(
(newOwner: UserID) => {
model.setOwner(newOwner, () => toast.success(information.changesSaved));
@ -372,7 +363,6 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
isProcessing: model.processing,
isAttachedToOSS: false,
toggleSubscribe,
setOwner,
setAccessPolicy,
promptEditors,

View File

@ -109,16 +109,6 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} />
</Tooltip>
<LabeledValue
id='sub_stats' //
className='sm:mb-1'
label='Отслеживают'
text={item?.subscribers.length ?? 0}
/>
<Tooltip anchorSelect='#sub_stats' layer='z-modalTooltip'>
<InfoUsers items={item?.subscribers ?? []} prefix={prefixes.user_subs} />
</Tooltip>
<LabeledValue
className='sm:mb-1'
label='Дата обновления'

View File

@ -4,7 +4,6 @@ import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext';
import { useRSForm } from '@/context/RSFormContext';
import { globals } from '@/utils/constants';
@ -21,8 +20,7 @@ interface EditorRSFormCardProps {
}
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
const { schema, isSubscribed } = useRSForm();
const { user } = useAuth();
const { schema } = useRSForm();
const controller = useRSEdit();
function initiateSubmit() {
@ -44,9 +42,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
return (
<>
<ToolbarRSFormCard
subscribed={isSubscribed}
modified={isModified}
anonymous={!user}
onSubmit={initiateSubmit}
onDestroy={onDestroy}
controller={controller}

View File

@ -2,7 +2,6 @@
import { useMemo } from 'react';
import { SubscribeIcon } from '@/components/DomainIcons';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
@ -20,21 +19,12 @@ import { IRSEditContext } from '../RSEditContext';
interface ToolbarRSFormCardProps {
modified: boolean;
subscribed: boolean;
anonymous: boolean;
onSubmit: () => void;
onDestroy: () => void;
controller: ILibraryItemEditor;
}
function ToolbarRSFormCard({
modified,
anonymous,
controller,
subscribed,
onSubmit,
onDestroy
}: ToolbarRSFormCardProps) {
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) {
const { accessLevel } = useAccessMode();
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
@ -71,14 +61,6 @@ function ToolbarRSFormCard({
onClick={controller.share}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
/>
{!anonymous ? (
<MiniButton
titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`}
icon={<SubscribeIcon value={subscribed} className={subscribed ? 'icon-primary' : 'clr-text-controls'} />}
disabled={controller.isProcessing}
onClick={controller.toggleSubscribe}
/>
) : null}
{controller.isMutable ? (
<MiniButton
title='Удалить схему'

View File

@ -74,7 +74,6 @@ export interface IRSEditContext extends ILibraryItemEditor {
setAccessPolicy: (newPolicy: AccessPolicy) => void;
promptEditors: () => void;
promptLocation: () => void;
toggleSubscribe: () => void;
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
select: (target: ConstituentaID) => void;
@ -589,14 +588,6 @@ export const RSEditState = ({
.catch(console.error);
}, []);
const toggleSubscribe = useCallback(() => {
if (model.isSubscribed) {
model.unsubscribe(() => toast.success(information.unsubscribed));
} else {
model.subscribe(() => toast.success(information.subscribed));
}
}, [model]);
const setOwner = useCallback(
(newOwner: UserID) => {
model.setOwner(newOwner, () => toast.success(information.changesSaved));
@ -632,7 +623,6 @@ export const RSEditState = ({
nothingSelected,
canDeleteSelected,
toggleSubscribe,
setOwner,
setAccessPolicy,
promptEditors,

View File

@ -1,85 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { urls } from '@/app/urls';
import DataTable, { createColumnHelper } from '@/components/ui/DataTable';
import NoData from '@/components/ui/NoData';
import { useConceptNavigation } from '@/context/NavigationContext';
import { ILibraryItem } from '@/models/library';
import { animateSideView } from '@/styling/animations';
interface TableSubscriptionsProps {
items: ILibraryItem[];
}
const columnHelper = createColumnHelper<ILibraryItem>();
function TableSubscriptions({ items }: TableSubscriptionsProps) {
const router = useConceptNavigation();
const intl = useIntl();
const openRSForm = (item: ILibraryItem) => router.push(urls.schema(item.id));
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Шифр',
size: 200,
minSize: 200,
maxSize: 200,
enableSorting: true
}),
columnHelper.accessor('title', {
id: 'title',
header: 'Название',
minSize: 200,
size: 2000,
maxSize: 2000,
enableSorting: true
}),
columnHelper.accessor('time_update', {
id: 'time_update',
header: 'Обновлена',
minSize: 150,
size: 150,
maxSize: 150,
cell: props => (
<div className='text-sm whitespace-nowrap'>{new Date(props.getValue()).toLocaleString(intl.locale)}</div>
),
enableSorting: true
})
],
[intl]
);
return (
<motion.div
initial={{ ...animateSideView.initial }}
animate={{ ...animateSideView.animate }}
exit={{ ...animateSideView.exit }}
>
<h1 className='mb-6 select-none'>Отслеживаемые схемы</h1>
<DataTable
dense
noFooter
className='max-h-[23.8rem] cc-scroll-y text-sm border'
columns={columns}
data={items}
headPosition='0'
enableSorting
initialSorting={{
id: 'time_update',
desc: true
}}
noDataComponent={<NoData className='h-[10rem]'>Отслеживаемые схемы отсутствуют</NoData>}
onRowClicked={openRSForm}
/>
</motion.div>
);
}
export default TableSubscriptions;

View File

@ -1,31 +1,14 @@
'use client';
import { AnimatePresence } from 'framer-motion';
import { useMemo, useState } from 'react';
import { SubscribeIcon } from '@/components/DomainIcons';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import AnimateFade from '@/components/wrap/AnimateFade';
import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useUserProfile } from '@/context/UserProfileContext';
import EditorPassword from './EditorPassword';
import EditorProfile from './EditorProfile';
import TableSubscriptions from './TableSubscriptions';
function UserContents() {
const { user, error, loading } = useUserProfile();
const { user: auth } = useAuth();
const { items } = useLibrary();
const [showSubs, setShowSubs] = useState(false);
const subscriptions = useMemo(() => {
return items.filter(item => auth?.subscriptions.includes(item.id));
}, [auth, items]);
return (
<DataLoader
@ -36,22 +19,12 @@ function UserContents() {
>
<AnimateFade className='flex gap-6 py-2 mx-auto w-fit'>
<div className='w-fit'>
<Overlay position='top-0 right-0'>
<MiniButton
title='Отслеживаемые схемы'
icon={<SubscribeIcon value={showSubs} className='icon-primary' />}
onClick={() => setShowSubs(prev => !prev)}
/>
</Overlay>
<h1 className='mb-4 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
</div>
<AnimatePresence>
{subscriptions.length > 0 && showSubs ? <TableSubscriptions items={subscriptions} /> : null}
</AnimatePresence>
</AnimateFade>
</DataLoader>
);

View File

@ -168,6 +168,7 @@ export function findReferenceAt(pos: number, state: EditorState) {
export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean) {
const dom = document.createElement('div');
dom.className = clsx(
'z-topmost',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2',
@ -246,6 +247,7 @@ export function domTooltipEntityReference(
) {
const dom = document.createElement('div');
dom.className = clsx(
'z-topmost',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col',
@ -304,6 +306,7 @@ export function domTooltipSyntacticReference(
) {
const dom = document.createElement('div');
dom.className = clsx(
'z-topmost',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col',

View File

@ -365,6 +365,8 @@ export function labelHelpTopic(topic: HelpTopic): string {
switch (topic) {
case HelpTopic.MAIN: return 'Портал';
case HelpTopic.THESAURUS: return 'Тезаурус';
case HelpTopic.INTERFACE: return 'Интерфейс';
case HelpTopic.UI_LIBRARY: return 'Библиотека';
case HelpTopic.UI_RS_MENU: return 'Меню схемы';
@ -414,6 +416,8 @@ export function describeHelpTopic(topic: HelpTopic): string {
switch (topic) {
case HelpTopic.MAIN: return 'общая справка по порталу';
case HelpTopic.THESAURUS: return 'термины Портала';
case HelpTopic.INTERFACE: return 'описание интерфейса пользователя';
case HelpTopic.UI_LIBRARY: return 'поиск и просмотр схем';
case HelpTopic.UI_RS_MENU: return 'меню редактирования схемы';
@ -920,9 +924,6 @@ export function describeOperationType(itemType: OperationType): string {
export const information = {
changesSaved: 'Изменения сохранены',
subscribed: 'Отслеживание отключено',
unsubscribed: 'Отслеживание выключено',
pathReady: 'Путь скопирован',
substituteSingle: 'Отождествление завершено',
reorderComplete: 'Упорядочение завершено',