Add option to clone only selected constituents

This commit is contained in:
IRBorisov 2024-04-04 14:34:25 +03:00
parent 2d707eca72
commit 604578d9da
12 changed files with 188 additions and 70 deletions

View File

@ -1,28 +1,27 @@
!! This is not complete list of todos !! !! This is not complete list of TODOs !!
This list only contains global tech refactorings and tech debt
For more specific TODOs see comments in code For more specific TODOs see comments in code
[Functionality] [Functionality - PROGRESS]
- landing page - Landing page
- home page (user specific) - Home page (user specific)
- export PDF
- блок нотификаций пользователей
- блок синтеза
- статический анализ схемы - Operational synthesis schema as LibraryItem ?
- конфигурации правил для разных статусов
- Library organization, search and exploration. Consider new user experience - Draggable rows in constituents table
- поиск по содержимому КС в Библиотеке
- private projects and permissions. Consider cooperative editing
- draggable rows in constituents table
- Clickable IDs in RSEditor tooltips - Clickable IDs in RSEditor tooltips
- ARIA (accessibility considerations) - for now machine reading not supported - Library organization, search and exploration. Consider new user experience
- Private projects and permissions. Consider cooperative editing
- Rework access setup: project-based, user-based, enable sharing. Prevent enumerating access to private schemas by default
- rework access setup: project-based, user-based, enable sharing. Prevent enumerating access to private schemas by default [functionality - PLANNING]
- User notifications on edit - consider spam prevention and change aggregation
- Static analyzer for RSForm
- Content based search in Library
- Export PDF (Items list, Graph)
- ARIA (accessibility considerations) - for now machine reading not supported
- Internationalization - at least english version. Consider react.intl
[Tech] [Tech]
@ -41,7 +40,7 @@ For more specific TODOs see comments in code
[Research] [Research]
Research and consider integration Research and consider integration
- django-allauth - django-allauth - consider supporting popular auth providers
- drf-messages - drf-messages
- radix-ui - radix-ui

View File

@ -11,6 +11,7 @@ from .basics import (
) )
from .data_access import ( from .data_access import (
LibraryItemSerializer, LibraryItemSerializer,
LibraryItemCloneSerializer,
RSFormSerializer, RSFormSerializer,
RSFormParseSerializer, RSFormParseSerializer,
VersionSerializer, VersionSerializer,

View File

@ -256,6 +256,11 @@ class CstListSerializer(serializers.Serializer):
return attrs return attrs
class LibraryItemCloneSerializer(LibraryItemSerializer):
''' Serializer: LibraryItem cloning. '''
items = PKField(many=True, required=False, queryset=Constituenta.objects.all())
class CstMoveSerializer(CstListSerializer): class CstMoveSerializer(CstListSerializer):
''' Serializer: Change constituenta position. ''' ''' Serializer: Change constituenta position. '''
move_to = serializers.IntegerField() move_to = serializers.IntegerField()

View File

@ -80,6 +80,10 @@ class EndpointTester(APITestCase):
response = self.execute(data, **kwargs) response = self.execute(data, **kwargs)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
def assertCreated(self, data=None, **kwargs):
response = self.execute(data, **kwargs)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def assertBadData(self, data=None, **kwargs): def assertBadData(self, data=None, **kwargs):
response = self.execute(data, **kwargs) response = self.execute(data, **kwargs)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

View File

@ -31,6 +31,7 @@ class TestLibraryViewset(EndpointTester):
alias='T3', alias='T3',
is_common=True is_common=True
) )
self.invalid_item = 1337 + self.common.pk
@decl_endpoint('/api/library', method='post') @decl_endpoint('/api/library', method='post')
@ -49,6 +50,7 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}', method='patch') @decl_endpoint('/api/library/{item}', method='patch')
def test_update(self): def test_update(self):
data = {'id': self.unowned.id, 'title': 'New title'} data = {'id': self.unowned.id, 'title': 'New title'}
self.assertNotFound(data, item=self.invalid_item)
self.assertForbidden(data, item=self.unowned.id) self.assertForbidden(data, item=self.unowned.id)
data = {'id': self.owned.id, 'title': 'New title'} data = {'id': self.owned.id, 'title': 'New title'}
@ -71,6 +73,7 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/claim', method='post') @decl_endpoint('/api/library/{item}/claim', method='post')
def test_claim(self): def test_claim(self):
self.assertNotFound(item=self.invalid_item)
self.assertForbidden(item=self.owned.id) self.assertForbidden(item=self.owned.id)
self.owned.is_common = True self.owned.is_common = True
@ -127,6 +130,7 @@ class TestLibraryViewset(EndpointTester):
@decl_endpoint('/api/library/{item}/subscribe', method='post') @decl_endpoint('/api/library/{item}/subscribe', method='post')
def test_subscriptions(self): def test_subscriptions(self):
self.assertNotFound(item=self.invalid_item)
response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe') response = self.client.delete(f'/api/library/{self.unowned.id}/unsubscribe')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(self.user in self.unowned.subscribers()) self.assertFalse(self.user in self.unowned.subscribers())
@ -174,11 +178,31 @@ class TestLibraryViewset(EndpointTester):
) )
data = {'title': 'Title1337'} data = {'title': 'Title1337'}
self.assertNotFound(data, item=self.invalid_item)
self.assertCreated(data, item=self.unowned.id)
data = {'title': 'Title1338'}
response = self.execute(data, item=self.owned.id) response = self.execute(data, item=self.owned.id)
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], data['title']) self.assertEqual(response.data['title'], data['title'])
self.assertEqual(len(response.data['items']), 2)
self.assertEqual(response.data['items'][0]['alias'], x12.alias) self.assertEqual(response.data['items'][0]['alias'], x12.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw) self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved) self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)
self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw) self.assertEqual(response.data['items'][1]['term_raw'], d2.term_raw)
self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved) self.assertEqual(response.data['items'][1]['term_resolved'], d2.term_resolved)
data = {'title': 'Title1340', 'items': []}
response = self.execute(data, item=self.owned.id)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(len(response.data['items']), 0)
data = {'title': 'Title1341', 'items': [x12.pk]}
response = self.execute(data, item=self.owned.id)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['title'], data['title'])
self.assertEqual(len(response.data['items']), 1)
self.assertEqual(response.data['items'][0]['alias'], x12.alias)
self.assertEqual(response.data['items'][0]['term_raw'], x12.term_raw)
self.assertEqual(response.data['items'][0]['term_resolved'], x12.term_resolved)

View File

@ -81,9 +81,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
@extend_schema( @extend_schema(
summary='clone item including contents', summary='clone item including contents',
tags=['Library'], tags=['Library'],
request=s.LibraryItemSerializer, request=s.LibraryItemCloneSerializer,
responses={ responses={
c.HTTP_201_CREATED: s.RSFormParseSerializer, c.HTTP_201_CREATED: s.RSFormParseSerializer,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
) )
@ -91,7 +93,7 @@ class LibraryViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'], url_path='clone') @action(detail=True, methods=['post'], url_path='clone')
def clone(self, request: Request, pk): def clone(self, request: Request, pk):
''' Endpoint: Create deep copy of library item. ''' ''' Endpoint: Create deep copy of library item. '''
serializer = s.LibraryItemSerializer(data=request.data) serializer = s.LibraryItemCloneSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
item = self._get_item() item = self._get_item()
if item.item_type == m.LibraryItemType.RSFORM: if item.item_type == m.LibraryItemType.RSFORM:
@ -104,6 +106,13 @@ class LibraryViewSet(viewsets.ModelViewSet):
clone_data['comment'] = serializer.validated_data.get('comment', '') clone_data['comment'] = serializer.validated_data.get('comment', '')
clone_data['is_common'] = serializer.validated_data.get('is_common', False) clone_data['is_common'] = serializer.validated_data.get('is_common', False)
clone_data['is_canonical'] = serializer.validated_data.get('is_canonical', False) clone_data['is_canonical'] = serializer.validated_data.get('is_canonical', False)
if 'items' in request.data:
filtered_items = []
for cst in clone_data['items']:
if cst['entityUID'] in request.data['items']:
filtered_items.append(cst)
clone_data['items'] = filtered_items
clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True}) clone = s.RSFormTRSSerializer(data=clone_data, context={'load_meta': True})
clone.is_valid(raise_exception=True) clone.is_valid(raise_exception=True)
new_schema = clone.save() new_schema = clone.save()
@ -111,13 +120,17 @@ class LibraryViewSet(viewsets.ModelViewSet):
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data=s.RSFormParseSerializer(new_schema.item).data data=s.RSFormParseSerializer(new_schema.item).data
) )
return Response(status=c.HTTP_404_NOT_FOUND) return Response(status=c.HTTP_400_BAD_REQUEST)
@extend_schema( @extend_schema(
summary='claim item', summary='claim item',
tags=['Library'], tags=['Library'],
request=None, request=None,
responses={c.HTTP_200_OK: s.LibraryItemSerializer} responses={
c.HTTP_200_OK: s.LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
@ -139,7 +152,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
summary='subscribe to item', summary='subscribe to item',
tags=['Library'], tags=['Library'],
request=None, request=None,
responses={c.HTTP_204_NO_CONTENT: None} responses={
c.HTTP_204_NO_CONTENT: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def subscribe(self, request: Request, pk): def subscribe(self, request: Request, pk):
@ -152,7 +169,11 @@ class LibraryViewSet(viewsets.ModelViewSet):
summary='unsubscribe from item', summary='unsubscribe from item',
tags=['Library'], tags=['Library'],
request=None, request=None,
responses={c.HTTP_204_NO_CONTENT: None}, responses={
c.HTTP_204_NO_CONTENT: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
},
) )
@action(detail=True, methods=['delete']) @action(detail=True, methods=['delete'])
def unsubscribe(self, request: Request, pk): def unsubscribe(self, request: Request, pk):

View File

@ -41,7 +41,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='create constituenta', summary='create constituenta',
tags=['Constituenta'], tags=['Constituenta'],
request=s.CstCreateSerializer, request=s.CstCreateSerializer,
responses={c.HTTP_201_CREATED: s.NewCstResponse} responses={
c.HTTP_201_CREATED: s.NewCstResponse,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['post'], url_path='cst-create') @action(detail=True, methods=['post'], url_path='cst-create')
def cst_create(self, request: Request, pk): def cst_create(self, request: Request, pk):
@ -69,7 +73,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='produce the structure of a given constituenta', summary='produce the structure of a given constituenta',
tags=['RSForm'], tags=['RSForm'],
request=s.CstTargetSerializer, request=s.CstTargetSerializer,
responses={c.HTTP_200_OK: s.NewMultiCstResponse} responses={
c.HTTP_200_OK: s.NewMultiCstResponse,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['patch'], url_path='cst-produce-structure') @action(detail=True, methods=['patch'], url_path='cst-produce-structure')
def produce_structure(self, request: Request, pk): def produce_structure(self, request: Request, pk):
@ -101,7 +109,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='rename constituenta', summary='rename constituenta',
tags=['Constituenta'], tags=['Constituenta'],
request=s.CstRenameSerializer, request=s.CstRenameSerializer,
responses={c.HTTP_200_OK: s.NewCstResponse} responses={
c.HTTP_200_OK: s.NewCstResponse,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-rename') @action(detail=True, methods=['patch'], url_path='cst-rename')
@ -135,7 +147,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='substitute constituenta', summary='substitute constituenta',
tags=['RSForm'], tags=['RSForm'],
request=s.CstSubstituteSerializer, request=s.CstSubstituteSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@transaction.atomic @transaction.atomic
@action(detail=True, methods=['patch'], url_path='cst-substitute') @action(detail=True, methods=['patch'], url_path='cst-substitute')
@ -161,7 +177,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='delete constituents', summary='delete constituents',
tags=['RSForm'], tags=['RSForm'],
request=s.CstListSerializer, request=s.CstListSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['patch'], url_path='cst-delete-multiple') @action(detail=True, methods=['patch'], url_path='cst-delete-multiple')
def cst_delete_multiple(self, request: Request, pk): def cst_delete_multiple(self, request: Request, pk):
@ -183,7 +203,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='move constituenta', summary='move constituenta',
tags=['RSForm'], tags=['RSForm'],
request=s.CstMoveSerializer, request=s.CstMoveSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['patch'], url_path='cst-moveto') @action(detail=True, methods=['patch'], url_path='cst-moveto')
def cst_moveto(self, request: Request, pk): def cst_moveto(self, request: Request, pk):
@ -208,7 +232,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='reset aliases, update expressions and references', summary='reset aliases, update expressions and references',
tags=['RSForm'], tags=['RSForm'],
request=None, request=None,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['patch'], url_path='reset-aliases') @action(detail=True, methods=['patch'], url_path='reset-aliases')
def reset_aliases(self, request: Request, pk): def reset_aliases(self, request: Request, pk):
@ -224,7 +252,11 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='load data from TRS file', summary='load data from TRS file',
tags=['RSForm'], tags=['RSForm'],
request=s.RSFormUploadSerializer, request=s.RSFormUploadSerializer,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['patch'], url_path='load-trs') @action(detail=True, methods=['patch'], url_path='load-trs')
def load_trs(self, request: Request, pk): def load_trs(self, request: Request, pk):
@ -251,7 +283,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='get all constituents data from DB', summary='get all constituents data from DB',
tags=['RSForm'], tags=['RSForm'],
request=None, request=None,
responses={c.HTTP_200_OK: s.RSFormSerializer} responses={
c.HTTP_200_OK: s.RSFormSerializer,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def contents(self, request: Request, pk): def contents(self, request: Request, pk):
@ -266,7 +301,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='get all constituents data and parses', summary='get all constituents data and parses',
tags=['RSForm'], tags=['RSForm'],
request=None, request=None,
responses={c.HTTP_200_OK: s.RSFormParseSerializer} responses={
c.HTTP_200_OK: s.RSFormParseSerializer,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def details(self, request: Request, pk): def details(self, request: Request, pk):
@ -281,7 +319,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='check RSLang expression', summary='check RSLang expression',
tags=['RSForm', 'FormalLanguage'], tags=['RSForm', 'FormalLanguage'],
request=s.ExpressionSerializer, request=s.ExpressionSerializer,
responses={c.HTTP_200_OK: s.ExpressionParseSerializer}, responses={
c.HTTP_200_OK: s.ExpressionParseSerializer,
c.HTTP_404_NOT_FOUND: None
},
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def check(self, request: Request, pk): def check(self, request: Request, pk):
@ -300,7 +341,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='resolve text with references', summary='resolve text with references',
tags=['RSForm', 'NaturalLanguage'], tags=['RSForm', 'NaturalLanguage'],
request=s.TextSerializer, request=s.TextSerializer,
responses={c.HTTP_200_OK: s.ResolverSerializer} responses={
c.HTTP_200_OK: s.ResolverSerializer,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def resolve(self, request: Request, pk): def resolve(self, request: Request, pk):
@ -319,7 +363,10 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
summary='export as TRS file', summary='export as TRS file',
tags=['RSForm'], tags=['RSForm'],
request=None, request=None,
responses={(c.HTTP_200_OK, 'application/zip'): bytes} responses={
(c.HTTP_200_OK, 'application/zip'): bytes,
c.HTTP_404_NOT_FOUND: None
}
) )
@action(detail=True, methods=['get'], url_path='export-trs') @action(detail=True, methods=['get'], url_path='export-trs')
def export_trs(self, request: Request, pk): def export_trs(self, request: Request, pk):
@ -341,7 +388,10 @@ class TrsImportView(views.APIView):
summary='import TRS file into RSForm', summary='import TRS file into RSForm',
tags=['RSForm'], tags=['RSForm'],
request=s.FileSerializer, request=s.FileSerializer,
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None
}
) )
def post(self, request: Request): def post(self, request: Request):
data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME) data = utils.read_zipped_json(request.FILES['file'].file, utils.EXTEOR_INNER_FILENAME)
@ -364,7 +414,10 @@ class TrsImportView(views.APIView):
summary='create new RSForm empty or from file', summary='create new RSForm empty or from file',
tags=['RSForm'], tags=['RSForm'],
request=s.LibraryItemSerializer, request=s.LibraryItemSerializer,
responses={c.HTTP_201_CREATED: s.LibraryItemSerializer} responses={
c.HTTP_201_CREATED: s.LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None
}
) )
@api_view(['POST']) @api_view(['POST'])
def create_rsform(request: Request): def create_rsform(request: Request):

View File

@ -6,7 +6,7 @@ import { ErrorData } from '@/components/info/InfoError';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI'; import { matchLibraryItem } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous'; import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCreateData, IRSFormData } from '@/models/rsform'; import { IRSForm, IRSFormCloneData, IRSFormCreateData, IRSFormData } from '@/models/rsform';
import { loadRSFormData } from '@/models/rsformAPI'; import { loadRSFormData } from '@/models/rsformAPI';
import { import {
DataCallback, DataCallback,
@ -31,7 +31,7 @@ interface ILibraryContext {
applyFilter: (params: ILibraryFilter) => ILibraryItem[]; applyFilter: (params: ILibraryFilter) => ILibraryItem[];
retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void; retrieveTemplate: (templateID: number, callback: (schema: IRSForm) => void) => void;
createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void; createItem: (data: IRSFormCreateData, callback?: DataCallback<ILibraryItem>) => void;
cloneItem: (target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => void; cloneItem: (target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
destroyItem: (target: number, callback?: () => void) => void; destroyItem: (target: number, callback?: () => void) => void;
localUpdateItem: (data: ILibraryItem) => void; localUpdateItem: (data: ILibraryItem) => void;
@ -194,7 +194,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
); );
const cloneItem = useCallback( const cloneItem = useCallback(
(target: number, data: IRSFormCreateData, callback: DataCallback<IRSFormData>) => { (target: number, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => {
if (!user) { if (!user) {
return; return;
} }

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
@ -13,43 +13,38 @@ import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem } from '@/models/library';
import { cloneTitle } from '@/models/libraryAPI'; import { cloneTitle } from '@/models/libraryAPI';
import { IRSFormCreateData } from '@/models/rsform'; import { ConstituentaID, IRSFormCloneData } from '@/models/rsform';
interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> { interface DlgCloneLibraryItemProps extends Pick<ModalProps, 'hideWindow'> {
base: ILibraryItem; base: ILibraryItem;
selected: ConstituentaID[];
totalCount: number;
} }
function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) { function DlgCloneLibraryItem({ hideWindow, base, selected, totalCount }: DlgCloneLibraryItemProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const [title, setTitle] = useState(''); const [title, setTitle] = useState(cloneTitle(base));
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState(base.alias);
const [comment, setComment] = useState(''); const [comment, setComment] = useState(base.comment);
const [common, setCommon] = useState(false); const [common, setCommon] = useState(base.is_common);
const [canonical, setCanonical] = useState(false); const [onlySelected, setOnlySelected] = useState(false);
const { cloneItem } = useLibrary(); const { cloneItem } = useLibrary();
const canSubmit = useMemo(() => title !== '' && alias !== '', [title, alias]); const canSubmit = useMemo(() => title !== '' && alias !== '', [title, alias]);
useEffect(() => {
if (base) {
setTitle(cloneTitle(base));
setAlias(base.alias);
setComment(base.comment);
setCommon(base.is_common);
setCanonical(false);
}
}, [base, base?.title, base?.alias, base?.comment, base?.is_common]);
function handleSubmit() { function handleSubmit() {
const data: IRSFormCreateData = { const data: IRSFormCloneData = {
item_type: base.item_type, item_type: base.item_type,
title: title, title: title,
alias: alias, alias: alias,
comment: comment, comment: comment,
is_common: common, is_common: common,
is_canonical: canonical is_canonical: false
}; };
if (onlySelected) {
data.items = selected;
}
cloneItem(base.id, data, newSchema => { cloneItem(base.id, data, newSchema => {
toast.success(`Копия создана: ${newSchema.alias}`); toast.success(`Копия создана: ${newSchema.alias}`);
router.push(urls.schema(newSchema.id)); router.push(urls.schema(newSchema.id));
@ -78,11 +73,12 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
className='max-w-sm' className='max-w-sm'
onChange={event => setAlias(event.target.value)} onChange={event => setAlias(event.target.value)}
/> />
<TextArea <TextArea id='dlg_comment' label='Описание' value={comment} onChange={event => setComment(event.target.value)} />
id='dlg_comment' <Checkbox
label='Комментарий' id='dlg_only_selected'
value={comment} label={`Только выбранные конституенты [${selected.length} из ${totalCount}]`}
onChange={event => setComment(event.target.value)} value={onlySelected}
setValue={value => setOnlySelected(value)}
/> />
<Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} /> <Checkbox id='dlg_is_common' label='Общедоступная схема' value={common} setValue={value => setCommon(value)} />
</Modal> </Modal>

View File

@ -234,6 +234,13 @@ export interface IRSFormCreateData extends ILibraryUpdateData {
fileName?: string; fileName?: string;
} }
/**
* Represents data, used for cloning {@link IRSForm}.
*/
export interface IRSFormCloneData extends ILibraryUpdateData {
items?: ConstituentaID[];
}
/** /**
* Represents data, used for uploading {@link IRSForm} as file. * Represents data, used for uploading {@link IRSForm} as file.
*/ */

View File

@ -527,7 +527,14 @@ export const RSEditState = ({
{model.schema ? ( {model.schema ? (
<AnimatePresence> <AnimatePresence>
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null} {showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
{showClone ? <DlgCloneLibraryItem base={model.schema} hideWindow={() => setShowClone(false)} /> : null} {showClone ? (
<DlgCloneLibraryItem
base={model.schema}
hideWindow={() => setShowClone(false)}
selected={selected}
totalCount={model.schema.items.length}
/>
) : null}
{showCreateCst ? ( {showCreateCst ? (
<DlgCreateCst <DlgCreateCst
hideWindow={() => setShowCreateCst(false)} hideWindow={() => setShowCreateCst(false)}

View File

@ -34,6 +34,7 @@ import {
ICstUpdateData, ICstUpdateData,
IInlineSynthesisData, IInlineSynthesisData,
IProduceStructureResponse, IProduceStructureResponse,
IRSFormCloneData,
IRSFormCreateData, IRSFormCreateData,
IRSFormData, IRSFormData,
IRSFormUploadData, IRSFormUploadData,
@ -212,7 +213,7 @@ export function postNewRSForm(request: FrontExchange<IRSFormCreateData, ILibrary
}); });
} }
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCreateData, IRSFormData>) { export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
AxiosPost({ AxiosPost({
title: 'Clone RSForm', title: 'Clone RSForm',
endpoint: `/api/library/${target}/clone`, endpoint: `/api/library/${target}/clone`,