F: Implement RSForm and OSS attribute sync

This commit is contained in:
Ivan 2024-08-05 23:53:07 +03:00
parent c0d01957ff
commit 92a0453b18
14 changed files with 225 additions and 130 deletions

View File

@ -94,9 +94,9 @@ class LibraryItemDetailsSerializer(serializers.ModelSerializer):
class UserTargetSerializer(serializers.Serializer): class UserTargetSerializer(serializers.Serializer):
''' Serializer: Target single User. ''' ''' Serializer: Target single User. '''
user = PKField(many=False, queryset=User.objects.all()) user = PKField(many=False, queryset=User.objects.all().only('pk'))
class UsersListSerializer(serializers.Serializer): class UsersListSerializer(serializers.Serializer):
''' Serializer: List of Users. ''' ''' Serializer: List of Users. '''
users = PKField(many=True, queryset=User.objects.all()) users = PKField(many=True, queryset=User.objects.all().only('pk'))

View File

@ -183,57 +183,6 @@ class TestLibraryViewset(EndpointTester):
self.unowned.refresh_from_db() self.unowned.refresh_from_db()
self.assertEqual(self.unowned.location, data['location']) self.assertEqual(self.unowned.location, data['location'])
@decl_endpoint('/api/library/{item}/add-editor', method='patch')
def test_add_editor(self):
time_update = self.owned.time_update
data = {'user': self.invalid_user}
self.executeBadData(data=data, item=self.owned.pk)
data = {'user': self.user.pk}
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.time_update, time_update)
self.assertEqual(self.owned.editors(), [self.user])
self.executeOK(data=data)
self.assertEqual(self.owned.editors(), [self.user])
data = {'user': self.user2.pk}
self.executeOK(data=data)
self.assertEqual(set(self.owned.editors()), set([self.user, self.user2]))
@decl_endpoint('/api/library/{item}/remove-editor', method='patch')
def test_remove_editor(self):
time_update = self.owned.time_update
data = {'user': self.invalid_user}
self.executeBadData(data=data, item=self.owned.pk)
data = {'user': self.user.pk}
self.executeNotFound(data=data, item=self.invalid_item)
self.executeForbidden(data=data, item=self.unowned.pk)
self.executeOK(data=data, item=self.owned.pk)
self.owned.refresh_from_db()
self.assertEqual(self.owned.time_update, time_update)
self.assertEqual(self.owned.editors(), [])
Editor.add(item=self.owned, user=self.user)
self.executeOK(data=data)
self.assertEqual(self.owned.editors(), [])
Editor.add(item=self.owned, user=self.user)
Editor.add(item=self.owned, user=self.user2)
data = {'user': self.user2.pk}
self.executeOK(data=data)
self.assertEqual(self.owned.editors(), [self.user])
@decl_endpoint('/api/library/{item}/set-editors', method='patch') @decl_endpoint('/api/library/{item}/set-editors', method='patch')
def test_set_editors(self): def test_set_editors(self):
time_update = self.owned.time_update time_update = self.owned.time_update

View File

@ -13,6 +13,7 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from apps.oss.models import OperationSchema
from apps.rsform.models import RSForm from apps.rsform.models import RSForm
from apps.rsform.serializers import RSFormParseSerializer from apps.rsform.serializers import RSFormParseSerializer
from apps.users.models import User from apps.users.models import User
@ -52,8 +53,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
'set_owner', 'set_owner',
'set_access_policy', 'set_access_policy',
'set_location', 'set_location',
'add_editor',
'remove_editor',
'set_editors' 'set_editors'
]: ]:
access_level = permissions.ItemOwner access_level = permissions.ItemOwner
@ -165,29 +164,19 @@ class LibraryViewSet(viewsets.ModelViewSet):
item = self._get_item() item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data) serializer = s.UserTargetSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
new_owner = serializer.validated_data['user'] new_owner = serializer.validated_data['user'].pk
m.LibraryItem.objects.filter(pk=item.pk).update(owner=new_owner) if new_owner == item.owner_id:
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('owner')
for schema in owned_schemas:
schema.owner_id = new_owner
m.LibraryItem.objects.bulk_update(owned_schemas, ['owner'])
item.owner_id = new_owner
item.save(update_fields=['owner'])
@extend_schema(
summary='set AccessPolicy for item',
tags=['Library'],
request=s.AccessPolicySerializer,
responses={
c.HTTP_200_OK: None,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='set-access-policy')
def set_access_policy(self, request: Request, pk):
''' Endpoint: Set item AccessPolicy. '''
item = self._get_item()
serializer = s.AccessPolicySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
new_policy = serializer.validated_data['access_policy']
m.LibraryItem.objects.filter(pk=item.pk).update(access_policy=new_policy)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
@ -208,49 +197,52 @@ class LibraryViewSet(viewsets.ModelViewSet):
serializer = s.LocationSerializer(data=request.data) serializer = s.LocationSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
location: str = serializer.validated_data['location'] location: str = serializer.validated_data['location']
if location == item.location:
return Response(status=c.HTTP_200_OK)
if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff: if location.startswith(m.LocationHead.LIBRARY) and not self.request.user.is_staff:
return Response(status=c.HTTP_403_FORBIDDEN) return Response(status=c.HTTP_403_FORBIDDEN)
m.LibraryItem.objects.filter(pk=item.pk).update(location=location)
with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('location')
for schema in owned_schemas:
schema.location = location
m.LibraryItem.objects.bulk_update(owned_schemas, ['location'])
item.location = location
item.save(update_fields=['location'])
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(
summary='add editor for item', summary='set AccessPolicy for item',
tags=['Library'], tags=['Library'],
request=s.UserTargetSerializer, request=s.AccessPolicySerializer,
responses={ responses={
c.HTTP_200_OK: None, c.HTTP_200_OK: None,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None, c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
) )
@action(detail=True, methods=['patch'], url_path='add-editor') @action(detail=True, methods=['patch'], url_path='set-access-policy')
def add_editor(self, request: Request, pk): def set_access_policy(self, request: Request, pk):
''' Endpoint: Add editor for item. ''' ''' Endpoint: Set item AccessPolicy. '''
item = self._get_item() item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data) serializer = s.AccessPolicySerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
new_editor = serializer.validated_data['user'] new_policy = serializer.validated_data['access_policy']
m.Editor.add(item=item, user=new_editor) if new_policy == item.access_policy:
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
with transaction.atomic():
if item.item_type == m.LibraryItemType.OPERATION_SCHEMA:
owned_schemas = OperationSchema(item).owned_schemas().only('access_policy')
for schema in owned_schemas:
schema.access_policy = new_policy
m.LibraryItem.objects.bulk_update(owned_schemas, ['access_policy'])
item.access_policy = new_policy
item.save(update_fields=['access_policy'])
@extend_schema(
summary='remove editor for item',
tags=['Library'],
request=s.UserTargetSerializer,
responses={
c.HTTP_200_OK: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='remove-editor')
def remove_editor(self, request: Request, pk):
''' Endpoint: Remove editor for item. '''
item = self._get_item()
serializer = s.UserTargetSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
editor = serializer.validated_data['user']
m.Editor.remove(item=item, user=editor)
return Response(status=c.HTTP_200_OK) return Response(status=c.HTTP_200_OK)
@extend_schema( @extend_schema(

View File

@ -51,6 +51,14 @@ class OperationSchema:
''' Operation substitutions. ''' ''' Operation substitutions. '''
return Substitution.objects.filter(operation__oss=self.model) return Substitution.objects.filter(operation__oss=self.model)
def owned_schemas(self) -> QuerySet[LibraryItem]:
''' Get QuerySet containing all result schemas owned by current OSS. '''
return LibraryItem.objects.filter(
producer__oss=self.model,
owner_id=self.model.owner_id,
location=self.model.location
)
def update_positions(self, data: list[dict]): def update_positions(self, data: list[dict]):
''' Update positions. ''' ''' Update positions. '''
lookup = {x['id']: x for x in data} lookup = {x['id']: x for x in data}

View File

@ -1,2 +1,3 @@
''' Tests for REST API. ''' ''' Tests for REST API. '''
from .t_change_attributes import *
from .t_oss import * from .t_oss import *

View File

@ -0,0 +1,107 @@
''' Testing API: Change attributes of OSS and RSForms. '''
from rest_framework import status
from apps.library.models import AccessPolicy, LocationHead
from apps.oss.models import Operation, OperationSchema, OperationType
from apps.rsform.models import RSForm
from apps.users.models import User
from shared.EndpointTester import EndpointTester, decl_endpoint
class TestChangeAttributes(EndpointTester):
''' Testing LibraryItem view when OSS is associated with RSForms. '''
def setUp(self):
super().setUp()
self.user3 = User.objects.create(
username='UserTest3',
email='anotheranother@test.com',
password='password'
)
self.owned = OperationSchema.create(
title='Test',
alias='T1',
owner=self.user,
location=LocationHead.LIBRARY
)
self.owned_id = self.owned.model.pk
self.ks1 = RSForm.create(
alias='KS1',
title='Test1',
owner=self.user,
location=LocationHead.USER
)
self.ks2 = RSForm.create(
alias='KS2',
title='Test2',
owner=self.user2,
location=LocationHead.LIBRARY
)
self.operation1 = self.owned.create_operation(
alias='1',
operation_type=OperationType.INPUT,
result=self.ks1.model
)
self.operation2 = self.owned.create_operation(
alias='2',
operation_type=OperationType.INPUT,
result=self.ks2.model
)
self.operation3 = self.owned.create_operation(
alias='3',
operation_type=OperationType.SYNTHESIS
)
self.owned.execute_operation(self.operation3)
self.operation3.refresh_from_db()
self.ks3 = self.operation3.result
@decl_endpoint('/api/library/{item}/set-owner', method='patch')
def test_set_owner(self):
data = {'user': self.user3.pk}
self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db()
self.ks1.refresh_from_db()
self.ks2.refresh_from_db()
self.ks3.refresh_from_db()
self.assertEqual(self.owned.model.owner, self.user3)
self.assertEqual(self.ks1.model.owner, self.user)
self.assertEqual(self.ks2.model.owner, self.user2)
self.assertEqual(self.ks3.owner, self.user3)
@decl_endpoint('/api/library/{item}/set-location', method='patch')
def test_set_location(self):
data = {'location': '/U/temp'}
self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db()
self.ks1.refresh_from_db()
self.ks2.refresh_from_db()
self.ks3.refresh_from_db()
self.assertEqual(self.owned.model.location, data['location'])
self.assertNotEqual(self.ks1.model.location, data['location'])
self.assertNotEqual(self.ks2.model.location, data['location'])
self.assertEqual(self.ks3.location, data['location'])
@decl_endpoint('/api/library/{item}/set-access-policy', method='patch')
def test_set_access_policy(self):
data = {'access_policy': AccessPolicy.PROTECTED}
self.executeOK(data=data, item=self.owned_id)
self.owned.refresh_from_db()
self.ks1.refresh_from_db()
self.ks2.refresh_from_db()
self.ks3.refresh_from_db()
self.assertEqual(self.owned.model.access_policy, data['access_policy'])
self.assertNotEqual(self.ks1.model.access_policy, data['access_policy'])
self.assertNotEqual(self.ks2.model.access_policy, data['access_policy'])
self.assertEqual(self.ks3.access_policy, data['access_policy'])

View File

@ -250,9 +250,8 @@ LOGGING = {
'root': { 'root': {
'handlers': ['console'], 'handlers': ['console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO') 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO')
}, }
} }
if len(sys.argv) > 1 and sys.argv[1] == 'test': if len(sys.argv) > 1 and sys.argv[1] == 'test':
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)

View File

@ -1,4 +1,7 @@
''' Utils: base tester class for endpoints. ''' ''' Utils: base tester class for endpoints. '''
import logging
from django.db import connection
from rest_framework import status from rest_framework import status
from rest_framework.test import APIClient, APIRequestFactory, APITestCase from rest_framework.test import APIClient, APIRequestFactory, APITestCase
@ -40,6 +43,9 @@ class EndpointTester(APITestCase):
self.client = APIClient() self.client = APIClient()
self.client.force_authenticate(user=self.user) self.client.force_authenticate(user=self.user)
self.logger = logging.getLogger('django.db.backends')
self.logger.setLevel(logging.DEBUG)
def setUpFullUsers(self): def setUpFullUsers(self):
self.factory = APIRequestFactory() self.factory = APIRequestFactory()
self.user = User.objects.create_user( self.user = User.objects.create_user(
@ -71,6 +77,16 @@ class EndpointTester(APITestCase):
def logout(self): def logout(self):
self.client.logout() self.client.logout()
def start_db_log(self):
''' Warning! Do not use this second time before calling stop_db_log. '''
''' Warning! Do not forget to enable global logging in settings. '''
logging.disable(logging.NOTSET)
connection.force_debug_cursor = True
def stop_db_log(self):
connection.force_debug_cursor = False
logging.disable(logging.CRITICAL)
def set_params(self, **kwargs): def set_params(self, **kwargs):
''' Given named argument values resolve current endpoint_mask. ''' ''' Given named argument values resolve current endpoint_mask. '''
if self.endpoint_mask and len(kwargs) > 0: if self.endpoint_mask and len(kwargs) > 0:

View File

@ -17,25 +17,36 @@ interface MiniSelectorOSSProps {
function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) { function MiniSelectorOSS({ items, onSelect }: MiniSelectorOSSProps) {
const ossMenu = useDropdown(); const ossMenu = useDropdown();
function onToggle(event: CProps.EventMouse) {
if (items.length > 1) {
ossMenu.toggle();
} else {
onSelect(event, items[0]);
}
}
return ( return (
<div ref={ossMenu.ref} className='flex items-center'> <div ref={ossMenu.ref} className='flex items-center'>
<MiniButton <MiniButton
icon={<IconOSS size='1.25rem' className='icon-primary' />} icon={<IconOSS size='1.25rem' className='icon-primary' />}
title='Связанные операционные схемы' title='Операционные схемы'
hideTitle={ossMenu.isOpen} hideTitle={ossMenu.isOpen}
onClick={() => ossMenu.toggle()} onClick={onToggle}
/> />
<Dropdown isOpen={ossMenu.isOpen}> {items.length > 1 ? (
<Label text='Список ОСС' className='border-b px-3 py-1' /> <Dropdown isOpen={ossMenu.isOpen}>
{items.map((reference, index) => ( <Label text='Список ОСС' className='border-b px-3 py-1' />
<DropdownButton {items.map((reference, index) => (
className='min-w-[5rem]' <DropdownButton
key={`${prefixes.oss_list}${index}`} className='min-w-[5rem]'
text={reference.alias} key={`${prefixes.oss_list}${index}`}
onClick={event => onSelect(event, reference)} text={reference.alias}
/> onClick={event => onSelect(event, reference)}
))} />
</Dropdown> ))}
</Dropdown>
) : null}
</div> </div>
); );
} }

View File

@ -103,6 +103,7 @@ export interface ILibraryItemEditor {
isMutable: boolean; isMutable: boolean;
isProcessing: boolean; isProcessing: boolean;
isAttachedToOSS: boolean;
setOwner: (newOwner: UserID) => void; setOwner: (newOwner: UserID) => void;
setAccessPolicy: (newPolicy: AccessPolicy) => void; setAccessPolicy: (newPolicy: AccessPolicy) => void;

View File

@ -15,7 +15,7 @@ import DlgChangeLocation from '@/dialogs/DlgChangeLocation';
import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation';
import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditEditors from '@/dialogs/DlgEditEditors';
import DlgEditOperation from '@/dialogs/DlgEditOperation'; import DlgEditOperation from '@/dialogs/DlgEditOperation';
import { AccessPolicy, LibraryItemID } from '@/models/library'; import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
import { Position2D } from '@/models/miscellaneous'; import { Position2D } from '@/models/miscellaneous';
import { import {
IOperationCreateData, IOperationCreateData,
@ -37,12 +37,13 @@ export interface ICreateOperationPrompt {
callback: (newID: OperationID) => void; callback: (newID: OperationID) => void;
} }
export interface IOssEditContext { export interface IOssEditContext extends ILibraryItemEditor {
schema?: IOperationSchema; schema?: IOperationSchema;
selected: OperationID[]; selected: OperationID[];
isMutable: boolean; isMutable: boolean;
isProcessing: boolean; isProcessing: boolean;
isAttachedToOSS: boolean;
showTooltip: boolean; showTooltip: boolean;
setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>; setShowTooltip: React.Dispatch<React.SetStateAction<boolean>>;
@ -319,6 +320,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
isMutable, isMutable,
isProcessing: model.processing, isProcessing: model.processing,
isAttachedToOSS: false,
toggleSubscribe, toggleSubscribe,
setOwner, setOwner,

View File

@ -4,6 +4,7 @@ import { useIntl } from 'react-intl';
import { IconEdit } from '@/components/Icons'; import { IconEdit } from '@/components/Icons';
import InfoUsers from '@/components/info/InfoUsers'; import InfoUsers from '@/components/info/InfoUsers';
import SelectUser from '@/components/select/SelectUser'; import SelectUser from '@/components/select/SelectUser';
import LabeledValue from '@/components/ui/LabeledValue';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
@ -15,8 +16,6 @@ import { UserID, UserLevel } from '@/models/user';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { prompts } from '@/utils/labels'; import { prompts } from '@/utils/labels';
import LabeledValue from '@/components/ui/LabeledValue';
interface EditorLibraryItemProps { interface EditorLibraryItemProps {
item?: ILibraryItemData; item?: ILibraryItemData;
isModified?: boolean; isModified?: boolean;
@ -48,11 +47,11 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
{accessLevel >= UserLevel.OWNER ? ( {accessLevel >= UserLevel.OWNER ? (
<Overlay position='top-[-0.5rem] left-[2.3rem] cc-icons'> <Overlay position='top-[-0.5rem] left-[2.3rem] cc-icons'>
<MiniButton <MiniButton
title='Изменить путь' title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Изменить путь'}
noHover noHover
onClick={() => controller.promptLocation()} onClick={() => controller.promptLocation()}
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />} icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
disabled={isModified || controller.isProcessing} disabled={isModified || controller.isProcessing || controller.isAttachedToOSS}
/> />
</Overlay> </Overlay>
) : null} ) : null}
@ -66,11 +65,11 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
<Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'> <Overlay position='top-[-0.5rem] left-[5.5rem] cc-icons'>
<div className='flex items-start'> <div className='flex items-start'>
<MiniButton <MiniButton
title='Изменить владельца' title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Изменить владельца'}
noHover noHover
onClick={() => ownerSelector.toggle()} onClick={() => ownerSelector.toggle()}
icon={<IconEdit size='1rem' className='mt-1 icon-primary' />} icon={<IconEdit size='1rem' className='mt-1 icon-primary' />}
disabled={isModified || controller.isProcessing} disabled={isModified || controller.isProcessing || controller.isAttachedToOSS}
/> />
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<SelectUser <SelectUser

View File

@ -33,7 +33,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
<Label text='Доступ' className='self-center select-none' /> <Label text='Доступ' className='self-center select-none' />
<div className='ml-auto cc-icons'> <div className='ml-auto cc-icons'>
<SelectAccessPolicy <SelectAccessPolicy
disabled={accessLevel <= UserLevel.EDITOR || controller.isProcessing} disabled={accessLevel <= UserLevel.EDITOR || controller.isProcessing || controller.isAttachedToOSS}
value={policy} value={policy}
onChange={newPolicy => controller.setAccessPolicy(newPolicy)} onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
/> />

View File

@ -26,6 +26,7 @@ import DlgSubstituteCst from '@/dialogs/DlgSubstituteCst';
import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm'; import DlgUploadRSForm from '@/dialogs/DlgUploadRSForm';
import { import {
AccessPolicy, AccessPolicy,
ILibraryItemEditor,
ILibraryUpdateData, ILibraryUpdateData,
IVersionData, IVersionData,
LibraryItemID, LibraryItemID,
@ -55,13 +56,14 @@ import { promptUnsaved } from '@/utils/utils';
import { RSTabID } from './RSTabs'; import { RSTabID } from './RSTabs';
export interface IRSEditContext { export interface IRSEditContext extends ILibraryItemEditor {
schema?: IRSForm; schema?: IRSForm;
selected: ConstituentaID[]; selected: ConstituentaID[];
isMutable: boolean; isMutable: boolean;
isContentEditable: boolean; isContentEditable: boolean;
isProcessing: boolean; isProcessing: boolean;
isAttachedToOSS: boolean;
canProduceStructure: boolean; canProduceStructure: boolean;
nothingSelected: boolean; nothingSelected: boolean;
canDeleteSelected: boolean; canDeleteSelected: boolean;
@ -153,6 +155,13 @@ export const RSEditState = ({
() => !nothingSelected && selected.every(id => !model.schema?.cstByID.get(id)?.is_inherited), () => !nothingSelected && selected.every(id => !model.schema?.cstByID.get(id)?.is_inherited),
[selected, nothingSelected, model.schema] [selected, nothingSelected, model.schema]
); );
const isAttachedToOSS = useMemo(
() =>
!!model.schema &&
model.schema.oss.length > 0 &&
(model.schema.stats.count_inherited > 0 || model.schema.items.length === 0),
[model.schema]
);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showClone, setShowClone] = useState(false); const [showClone, setShowClone] = useState(false);
@ -618,6 +627,7 @@ export const RSEditState = ({
isMutable, isMutable,
isContentEditable, isContentEditable,
isProcessing: model.processing, isProcessing: model.processing,
isAttachedToOSS,
canProduceStructure, canProduceStructure,
nothingSelected, nothingSelected,
canDeleteSelected, canDeleteSelected,