Refactor async functions. Implement DeleteCst

This commit is contained in:
IRBorisov 2023-07-23 21:38:04 +03:00
parent e58fd183e9
commit ffbeafc3f5
21 changed files with 324 additions and 121 deletions

View File

@ -92,6 +92,7 @@ class RSForm(models.Model):
csttype=type csttype=type
) )
self._recreate_order() self._recreate_order()
self.save()
return Constituenta.objects.get(pk=result.pk) return Constituenta.objects.get(pk=result.pk)
@transaction.atomic @transaction.atomic
@ -107,8 +108,17 @@ class RSForm(models.Model):
csttype=type csttype=type
) )
self._recreate_order() self._recreate_order()
self.save()
return Constituenta.objects.get(pk=result.pk) return Constituenta.objects.get(pk=result.pk)
@transaction.atomic
def delete_cst(self, listCst):
''' Delete multiple constituents. Do not check if listCst are from this schema '''
for cst in listCst:
cst.delete()
self._recreate_order()
self.save()
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm': def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm':

View File

@ -7,6 +7,12 @@ class FileSerializer(serializers.Serializer):
file = serializers.FileField(allow_empty_file=False) file = serializers.FileField(allow_empty_file=False)
class ItemsListSerlializer(serializers.Serializer):
items = serializers.ListField(
child=serializers.IntegerField()
)
class ExpressionSerializer(serializers.Serializer): class ExpressionSerializer(serializers.Serializer):
expression = serializers.CharField() expression = serializers.CharField()

View File

@ -177,6 +177,20 @@ class TestRSForm(TestCase):
self.assertEqual(cst2.schema, schema) self.assertEqual(cst2.schema, schema)
self.assertEqual(cst1.order, 1) self.assertEqual(cst1.order, 1)
def test_delete_cst(self):
schema = RSForm.objects.create(title='Test')
x1 = schema.insert_last('X1', CstType.BASE)
x2 = schema.insert_last('X2', CstType.BASE)
d1 = schema.insert_last('D1', CstType.TERM)
d2 = schema.insert_last('D2', CstType.TERM)
schema.delete_cst([x2, d1])
x1.refresh_from_db()
d2.refresh_from_db()
schema.refresh_from_db()
self.assertEqual(schema.constituents().count(), 2)
self.assertEqual(x1.order, 1)
self.assertEqual(d2.order, 2)
def test_to_json(self): def test_to_json(self):
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test') schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
x1 = schema.insert_at(4, 'X1', CstType.BASE) x1 = schema.insert_at(4, 'X1', CstType.BASE)

View File

@ -173,14 +173,14 @@ class TestRSFormViewset(APITestCase):
def test_create_constituenta(self): def test_create_constituenta(self):
data = json.dumps({'alias': 'X3', 'csttype': 'basic'}) data = json.dumps({'alias': 'X3', 'csttype': 'basic'})
response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/new-constituenta/', response = self.client.post(f'/api/rsforms/{self.rsform_unowned.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
schema = self.rsform_owned schema = self.rsform_owned
Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1) Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2) x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
response = self.client.post(f'/api/rsforms/{schema.id}/new-constituenta/', response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['alias'], 'X3') self.assertEqual(response.data['alias'], 'X3')
@ -188,13 +188,38 @@ class TestRSFormViewset(APITestCase):
self.assertEqual(x3.order, 3) self.assertEqual(x3.order, 3)
data = json.dumps({'alias': 'X4', 'csttype': 'basic', 'insert_after': x2.id}) data = json.dumps({'alias': 'X4', 'csttype': 'basic', 'insert_after': x2.id})
response = self.client.post(f'/api/rsforms/{schema.id}/new-constituenta/', response = self.client.post(f'/api/rsforms/{schema.id}/cst-create/',
data=data, content_type='application/json') data=data, content_type='application/json')
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['alias'], 'X4') self.assertEqual(response.data['alias'], 'X4')
x4 = Constituenta.objects.get(alias=response.data['alias']) x4 = Constituenta.objects.get(alias=response.data['alias'])
self.assertEqual(x4.order, 3) self.assertEqual(x4.order, 3)
def test_delete_constituenta(self):
schema = self.rsform_owned
data = json.dumps({'items': [1337]})
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 404)
x1 = Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
x2 = Constituenta.objects.create(schema=schema, alias='X2', csttype='basic', order=2)
data = json.dumps({'items': [x1.id]})
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
data=data, content_type='application/json')
x2.refresh_from_db()
schema.refresh_from_db()
self.assertEqual(response.status_code, 202)
self.assertEqual(schema.constituents().count(), 1)
self.assertEqual(x2.alias, 'X2')
self.assertEqual(x2.order, 1)
x3 = Constituenta.objects.create(schema=self.rsform_unowned, alias='X1', csttype='basic', order=1)
data = json.dumps({'items': [x3.id]})
response = self.client.post(f'/api/rsforms/{schema.id}/cst-multidelete/',
data=data, content_type='application/json')
self.assertEqual(response.status_code, 400)
class TestFunctionalViews(APITestCase): class TestFunctionalViews(APITestCase):
def setUp(self): def setUp(self):

View File

@ -42,7 +42,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update', if self.action in ['update', 'destroy', 'partial_update',
'new_constituenta']: 'cst_create', 'cst_multidelete']:
permission_classes = [utils.ObjectOwnerOrAdmin] permission_classes = [utils.ObjectOwnerOrAdmin]
elif self.action in ['create', 'claim']: elif self.action in ['create', 'claim']:
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
@ -50,9 +50,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny] permission_classes = [permissions.AllowAny]
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
@action(detail=True, methods=['post'], url_path='new-constituenta') @action(detail=True, methods=['post'], url_path='cst-create')
def new_constituenta(self, request, pk): def cst_create(self, request, pk):
''' View schema contents (including constituents) ''' ''' Create new constituenta '''
schema: models.RSForm = self.get_object() schema: models.RSForm = self.get_object()
serializer = serializers.NewConstituentaSerializer(data=request.data) serializer = serializers.NewConstituentaSerializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
@ -65,6 +65,26 @@ class RSFormViewSet(viewsets.ModelViewSet):
constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['csttype']) constituenta = schema.insert_last(serializer.validated_data['alias'], serializer.validated_data['csttype'])
return Response(status=201, data=constituenta.to_json()) return Response(status=201, data=constituenta.to_json())
@action(detail=True, methods=['post'], url_path='cst-multidelete')
def cst_multidelete(self, request, pk):
''' Delete multiple constituents '''
schema: models.RSForm = self.get_object()
serializer = serializers.ItemsListSerlializer(data=request.data)
serializer.is_valid(raise_exception=True)
listCst = []
# TODO: consider moving validation to serializer
try:
for id in serializer.validated_data['items']:
cst = models.Constituenta.objects.get(pk=id)
if (cst.schema != schema):
return Response({'error', 'Конституенты должны относиться к данной схеме'}, status=400)
listCst.append(cst)
except models.Constituenta.DoesNotExist:
return Response(status=404)
schema.delete_cst(listCst)
return Response(status=202)
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def claim(self, request, pk=None): def claim(self, request, pk=None):
schema: models.RSForm = self.get_object() schema: models.RSForm = self.get_object()

View File

@ -7,9 +7,9 @@ import { getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI'
interface IAuthContext { interface IAuthContext {
user: ICurrentUser | undefined user: ICurrentUser | undefined
login: (username: string, password: string, onSuccess?: () => void) => void login: (username: string, password: string) => Promise<void>
logout: (onSuccess?: () => void) => void logout: (onSuccess?: () => void) => Promise<void>
signup: (data: IUserSignupData, onSuccess?: () => void) => void signup: (data: IUserSignupData) => Promise<void>
loading: boolean loading: boolean
error: ErrorInfo error: ErrorInfo
setError: (error: ErrorInfo) => void setError: (error: ErrorInfo) => void
@ -17,9 +17,9 @@ interface IAuthContext {
export const AuthContext = createContext<IAuthContext>({ export const AuthContext = createContext<IAuthContext>({
user: undefined, user: undefined,
login: () => {}, login: async () => {},
logout: () => {}, logout: async () => {},
signup: () => {}, signup: async () => {},
loading: false, loading: false,
error: '', error: '',
setError: () => {} setError: () => {}
@ -63,18 +63,17 @@ export const AuthState = ({ children }: AuthStateProps) => {
}); });
} }
async function logout(onSuccess?: () => void) { async function logout() {
setError(undefined); setError(undefined);
postLogout({ postLogout({
showError: true, showError: true,
onSucccess: response => { onSucccess: response => {
loadCurrentUser(); loadCurrentUser();
if(onSuccess) onSuccess();
} }
}); });
} }
async function signup(data: IUserSignupData, onSuccess?: () => void) { async function signup(data: IUserSignupData) {
setError(undefined); setError(undefined);
postSignup({ postSignup({
data: data, data: data,
@ -83,7 +82,6 @@ export const AuthState = ({ children }: AuthStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: response => { onSucccess: response => {
loadCurrentUser(); loadCurrentUser();
if(onSuccess) onSuccess();
} }
}); });
} }

View File

@ -3,7 +3,7 @@ import { IConstituenta, IRSForm } from '../utils/models';
import { useRSFormDetails } from '../hooks/useRSFormDetails'; import { useRSFormDetails } from '../hooks/useRSFormDetails';
import { ErrorInfo } from '../components/BackendError'; import { ErrorInfo } from '../components/BackendError';
import { useAuth } from './AuthContext'; import { useAuth } from './AuthContext';
import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm, postNewConstituenta } from '../utils/backendAPI'; import { BackendCallback, deleteRSForm, getTRSFile, patchConstituenta, patchRSForm, postClaimRSForm, postDeleteConstituenta, postNewConstituenta } from '../utils/backendAPI';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
interface IRSFormContext { interface IRSFormContext {
@ -23,14 +23,16 @@ interface IRSFormContext {
toggleForceAdmin: () => void toggleForceAdmin: () => void
toggleReadonly: () => void toggleReadonly: () => void
toggleTracking: () => void toggleTracking: () => void
reload: () => void
update: (data: any, callback?: BackendCallback) => void
destroy: (callback: BackendCallback) => void
claim: (callback: BackendCallback) => void
download: (callback: BackendCallback) => void
cstUpdate: (data: any, callback: BackendCallback) => void reload: () => Promise<void>
cstCreate: (data: any, callback: BackendCallback) => void update: (data: any, callback?: BackendCallback) => Promise<void>
destroy: (callback?: BackendCallback) => Promise<void>
claim: (callback?: BackendCallback) => Promise<void>
download: (callback: BackendCallback) => Promise<void>
cstUpdate: (data: any, callback?: BackendCallback) => Promise<void>
cstCreate: (data: any, callback?: BackendCallback) => Promise<void>
cstDelete: (data: any, callback?: BackendCallback) => Promise<void>
} }
export const RSFormContext = createContext<IRSFormContext>({ export const RSFormContext = createContext<IRSFormContext>({
@ -50,14 +52,16 @@ export const RSFormContext = createContext<IRSFormContext>({
toggleForceAdmin: () => {}, toggleForceAdmin: () => {},
toggleReadonly: () => {}, toggleReadonly: () => {},
toggleTracking: () => {}, toggleTracking: () => {},
reload: () => {},
update: () => {},
destroy: () => {},
claim: () => {},
download: () => {},
cstUpdate: () => {}, reload: async () => {},
cstCreate: () => {}, update: async () => {},
destroy: async () => {},
claim: async () => {},
download: async () => {},
cstUpdate: async () => {},
cstCreate: async () => {},
cstDelete: async () => {},
}) })
interface RSFormStateProps { interface RSFormStateProps {
@ -76,22 +80,26 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema]); const isOwned = useMemo(() => user?.id === schema?.owner || false, [user, schema]);
const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]); const isClaimable = useMemo(() => (user?.id !== schema?.owner || false), [user, schema]);
const isEditable = useMemo(() => { const isEditable = useMemo(
() => {
return ( return (
!readonly && !loading && !readonly &&
(isOwned || (forceAdmin && user?.is_staff) || false) (isOwned || (forceAdmin && user?.is_staff) || false)
) )
}, [user, readonly, forceAdmin, isOwned]); }, [user, readonly, forceAdmin, isOwned, loading]);
const isTracking = useMemo(() => { const isTracking = useMemo(
() => {
return true; return true;
}, []); }, []);
const toggleTracking = useCallback(() => { const toggleTracking = useCallback(
() => {
toast('not implemented yet'); toast('not implemented yet');
}, []); }, []);
async function update(data: any, callback?: BackendCallback) { const update = useCallback(
async (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined);
patchRSForm(schemaID, { patchRSForm(schemaID, {
data: data, data: data,
@ -100,9 +108,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [schemaID, setError]);
async function destroy(callback: BackendCallback) { const destroy = useCallback(
async (callback?: BackendCallback) => {
setError(undefined); setError(undefined);
deleteRSForm(schemaID, { deleteRSForm(schemaID, {
showError: true, showError: true,
@ -110,9 +119,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [schemaID, setError]);
async function claim(callback: BackendCallback) { const claim = useCallback(
async (callback?: BackendCallback) => {
setError(undefined); setError(undefined);
postClaimRSForm(schemaID, { postClaimRSForm(schemaID, {
showError: true, showError: true,
@ -120,9 +130,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [schemaID, setError]);
async function download(callback: BackendCallback) { const download = useCallback(
async (callback: BackendCallback) => {
setError(undefined); setError(undefined);
getTRSFile(schemaID, { getTRSFile(schemaID, {
showError: true, showError: true,
@ -130,9 +141,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [schemaID, setError]);
async function cstUpdate(data: any, callback?: BackendCallback) { const cstUpdate = useCallback(
async (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined);
patchConstituenta(String(active!.entityUID), { patchConstituenta(String(active!.entityUID), {
data: data, data: data,
@ -141,9 +153,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [active, setError]);
async function cstCreate(data: any, callback?: BackendCallback) { const cstCreate = useCallback(
async (data: any, callback?: BackendCallback) => {
setError(undefined); setError(undefined);
postNewConstituenta(schemaID, { postNewConstituenta(schemaID, {
data: data, data: data,
@ -152,7 +165,19 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
onError: error => setError(error), onError: error => setError(error),
onSucccess: callback onSucccess: callback
}); });
} }, [schemaID, setError]);
const cstDelete = useCallback(
async (data: any, callback?: BackendCallback) => {
setError(undefined);
postDeleteConstituenta(schemaID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: error => setError(error),
onSucccess: callback
});
}, [schemaID, setError]);
return ( return (
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{
@ -164,7 +189,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
isOwned, isEditable, isClaimable, isOwned, isEditable, isClaimable,
isTracking, toggleTracking, isTracking, toggleTracking,
reload, update, download, destroy, claim, reload, update, download, destroy, claim,
cstUpdate, cstCreate cstUpdate, cstCreate, cstDelete,
}}> }}>
{ children } { children }
</RSFormContext.Provider> </RSFormContext.Provider>

View File

@ -5,13 +5,13 @@ import { getActiveUsers } from '../utils/backendAPI'
interface IUsersContext { interface IUsersContext {
users: IUserInfo[] users: IUserInfo[]
reload: () => void reload: () => Promise<void>
getUserLabel: (userID?: number) => string getUserLabel: (userID?: number) => string
} }
export const UsersContext = createContext<IUsersContext>({ export const UsersContext = createContext<IUsersContext>({
users: [], users: [],
reload: () => {}, reload: async () => {},
getUserLabel: () => '' getUserLabel: () => ''
}) })

View File

@ -8,7 +8,8 @@ export function useRSFormDetails({target}: {target?: string}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<ErrorInfo>(undefined); const [error, setError] = useState<ErrorInfo>(undefined);
const fetchData = useCallback(async () => { const fetchData = useCallback(
async () => {
setError(undefined); setError(undefined);
setSchema(undefined); setSchema(undefined);
if (!target) { if (!target) {
@ -20,17 +21,18 @@ export function useRSFormDetails({target}: {target?: string}) {
onError: error => setError(error), onError: error => setError(error),
onSucccess: (response) => { onSucccess: (response) => {
CalculateStats(response.data) CalculateStats(response.data)
console.log(response.data);
setSchema(response.data); setSchema(response.data);
} }
}); });
}, [target]); }, [target]);
async function reload() { async function reload() {
fetchData() fetchData();
} }
useEffect(() => { useEffect(() => {
fetchData() fetchData();
}, [fetchData]) }, [fetchData])
return { schema, reload, error, setError, loading }; return { schema, reload, error, setError, loading };

View File

@ -1,4 +1,15 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
function HomePage() { function HomePage() {
const navigate = useNavigate();
const {user} = useAuth();
if (user) {
navigate('/rsforms?filter=personal');
} else {
navigate('/rsforms?filter=common');
}
return ( return (
<div className='flex flex-col items-center justify-center w-full py-2'> <div className='flex flex-col items-center justify-center w-full py-2'>
<p>Home page</p> <p>Home page</p>

View File

@ -30,7 +30,8 @@ function LoginPage() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!loading) { if (!loading) {
login(username, password, () => { navigate('/rsforms?filter=personal'); }); login(username, password)
.then(() => navigate('/rsforms?filter=personal'));
} }
}; };

View File

@ -2,7 +2,7 @@ export function NotFoundPage() {
return ( return (
<div> <div>
<h1 className='text-xl font-semibold'>Error 404 - Not Found</h1> <h1 className='text-xl font-semibold'>Error 404 - Not Found</h1>
<p className='mt-2'>Данная страница не существует</p> <p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базы данных</p>
</div> </div>
); );
} }

View File

@ -1,19 +1,26 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { EditMode } from '../../utils/models'; import { CstType, EditMode, INewCstData } from '../../utils/models';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import ExpressionEditor from './ExpressionEditor'; import ExpressionEditor from './ExpressionEditor';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
import { getCstTypeLabel } from '../../utils/staticUI'; import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
import ConstituentsSideList from './ConstituentsSideList'; import ConstituentsSideList from './ConstituentsSideList';
import { SaveIcon } from '../../components/Icons'; import { DumpBinIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import CreateCstModal from './CreateCstModal';
import { AxiosResponse } from 'axios';
import { useNavigate } from 'react-router-dom';
import { RSFormTabsList } from './RSFormTabs';
function ConstituentEditor() { function ConstituentEditor() {
const navigate = useNavigate();
const { const {
active, schema, setActive, processing, cstUpdate, isEditable, reload active, schema, setActive, processing, isEditable, reload,
cstDelete, cstUpdate, cstCreate
} = useRSForm(); } = useRSForm();
const [showCstModal, setShowCstModal] = useState(false);
const [editMode, setEditMode] = useState(EditMode.TEXT); const [editMode, setEditMode] = useState(EditMode.TEXT);
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -42,7 +49,8 @@ function ConstituentEditor() {
} }
}, [active]); }, [active]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit =
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if (!processing) { if (!processing) {
const data = { const data = {
@ -59,14 +67,51 @@ function ConstituentEditor() {
'forms': active?.term?.forms || [], 'forms': active?.term?.forms || [],
} }
}; };
cstUpdate(data, (response) => { cstUpdate(data)
console.log(response); .then(() => {
toast.success('Изменения сохранены'); toast.success('Изменения сохранены');
reload(); reload();
}); });
} }
}; };
const handleDelete = useCallback(
async () => {
if (!active || !window.confirm('Вы уверены, что хотите удалить конституенту?')) {
return;
}
const data = {
'items': [active.entityUID]
}
const index = schema?.items?.indexOf(active)
await cstDelete(data);
if (schema?.items && index && index + 1 < schema?.items?.length) {
setActive(schema?.items[index + 1]);
}
toast.success(`Конституента удалена: ${active.alias}`);
reload();
}, [active, schema, setActive, cstDelete, reload]);
const handleAddNew = useCallback(
async (csttype?: CstType) => {
if (!active || !schema) {
return;
}
if (!csttype) {
setShowCstModal(true);
} else {
const data: INewCstData = {
'csttype': csttype,
'alias': createAliasFor(csttype, schema!),
'insert_after': active.entityUID
}
cstCreate(data, (response: AxiosResponse) => {
navigate(`/rsforms/${schema.id}?tab=${RSFormTabsList.CST_EDIT}&active=${response.data['entityUID']}`);
window.location.reload();
});
}
}, [active, schema, cstCreate, navigate]);
const handleRename = useCallback(() => { const handleRename = useCallback(() => {
toast.info('Переименование в разработке'); toast.info('Переименование в разработке');
}, []); }, []);
@ -78,8 +123,21 @@ function ConstituentEditor() {
return ( return (
<div className='flex items-start w-full gap-2'> <div className='flex items-start w-full gap-2'>
<CreateCstModal
show={showCstModal}
toggle={() => setShowCstModal(!showCstModal)}
onCreate={handleAddNew}
defaultType={active?.cstType as CstType}
/>
<form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min px-4 py-2 border'> <form onSubmit={handleSubmit} className='flex-grow min-w-[50rem] max-w-min px-4 py-2 border'>
<div className='flex items-start justify-between'> <div className='flex items-start justify-between'>
<button type='submit'
title='Сохранить изменения'
className='px-1 py-1 font-bold rounded whitespace-nowrap disabled:cursor-not-allowed clr-btn-primary'
disabled={!isEditable}
>
<SaveIcon size={5} />
</button>
<div className='flex items-start justify-center w-full gap-4'> <div className='flex items-start justify-center w-full gap-4'>
<span className='mr-12'> <span className='mr-12'>
<label <label
@ -103,13 +161,22 @@ function ConstituentEditor() {
</span> </span>
</div> </div>
<div className='flex justify-end'> <div className='flex justify-end'>
<button type='submit' <button type='button'
title='Сохранить изменения' title='Создать конституенты после данной'
className={'px-1 py-1 whitespace-nowrap font-bold disabled:cursor-not-allowed rounded clr-btn-primary'} className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
disabled={!isEditable} disabled={!isEditable}
> onClick={() => handleAddNew()}
<SaveIcon size={5} /> >
</button> <SmallPlusIcon size={5} color={isEditable ? 'text-green': ''} />
</button>
<button type='button'
title='Удалить редактируемую конституенту'
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
disabled={!isEditable}
onClick={handleDelete}
>
<DumpBinIcon size={5} color={isEditable ? 'text-red': ''} />
</button>
</div> </div>
</div> </div>
<TextArea id='term' label='Термин' <TextArea id='term' label='Термин'

View File

@ -129,7 +129,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
columns={columns} columns={columns}
keyField='id' keyField='id'
noContextMenu noContextMenu
noDataComponent={<span className='p-2 flex flex-col justify-center text-center'> noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
<p>Список конституент пуст</p> <p>Список конституент пуст</p>
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра</p>
</span>} </span>}

View File

@ -15,9 +15,9 @@ interface ConstituentsTableProps {
} }
function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) { function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
const { schema, isEditable, cstCreate, reload } = useRSForm(); const { schema, isEditable, cstCreate, cstDelete, reload } = useRSForm();
const [selectedRows, setSelectedRows] = useState<IConstituenta[]>([]); const [selected, setSelected] = useState<IConstituenta[]>([]);
const nothingSelected = useMemo(() => selectedRows.length === 0, [selectedRows]); const nothingSelected = useMemo(() => selected.length === 0, [selected]);
const [showCstModal, setShowCstModal] = useState(false); const [showCstModal, setShowCstModal] = useState(false);
@ -29,11 +29,21 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
}, [onOpenEdit]); }, [onOpenEdit]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
toast.info('Удаление конституент'); if (!window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) {
}, []); return;
}
const data = {
'items': selected.map(cst => cst.entityUID)
}
const deletedNamed = selected.map(cst => cst.alias)
cstDelete(data, (response: AxiosResponse) => {
reload().then(() => toast.success(`Конституенты удалены: ${deletedNamed}`));
});
}, [selected, cstDelete, reload]);
const handleMoveUp = useCallback(() => { const handleMoveUp = useCallback(() => {
toast.info('Перемещение вверх'); toast.info('Перемещение вверх');
}, []); }, []);
const handleMoveDown = useCallback(() => { const handleMoveDown = useCallback(() => {
@ -49,18 +59,17 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
setShowCstModal(true); setShowCstModal(true);
} else { } else {
let data: INewCstData = { let data: INewCstData = {
csttype: csttype, 'csttype': csttype,
alias: createAliasFor(csttype, schema!) 'alias': createAliasFor(csttype, schema!)
} }
if (selectedRows.length > 0) { if (selected.length > 0) {
data['insert_after'] = selectedRows[selectedRows.length - 1].entityUID data['insert_after'] = selected[selected.length - 1].entityUID
} }
cstCreate(data, (response: AxiosResponse) => { cstCreate(data, (response: AxiosResponse) => {
reload(); reload().then(() => toast.success(`Добавлена конституента ${response.data['alias']}`));
toast.info(`Добавлена конституента ${response.data['alias']}`);
}); });
} }
}, [schema, selectedRows, reload, cstCreate]); }, [schema, selected, reload, cstCreate]);
const columns = useMemo(() => const columns = useMemo(() =>
[ [
@ -182,7 +191,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
/> />
<div className='w-full'> <div className='w-full'>
<div className='sticky top-[4rem] z-10 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app'> <div className='sticky top-[4rem] z-10 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app'>
<div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selectedRows.length}</b> из {schema?.stats?.count_all || 0}</span></div> <div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selected.length}</b> из {schema?.stats?.count_all || 0}</span></div>
{isEditable && <div className='flex justify-start w-full gap-1'> {isEditable && <div className='flex justify-start w-full gap-1'>
<Button <Button
tooltip='Переместить вверх' tooltip='Переместить вверх'
@ -247,7 +256,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
selectableRows selectableRows
selectableRowsHighlight selectableRowsHighlight
onSelectedRowsChange={({selectedRows}) => setSelectedRows(selectedRows)} onSelectedRowsChange={({selectedRows}) => setSelected(selectedRows)}
onRowDoubleClicked={onOpenEdit} onRowDoubleClicked={onOpenEdit}
onRowClicked={handleRowClicked} onRowClicked={handleRowClicked}
dense dense

View File

@ -1,16 +1,17 @@
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import { CstType } from '../../utils/models'; import { CstType } from '../../utils/models';
import Select from 'react-select'; import Select from 'react-select';
import { CstTypeSelector } from '../../utils/staticUI'; import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface CreateCstModalProps { interface CreateCstModalProps {
show: boolean show: boolean
toggle: () => void toggle: () => void
defaultType?: CstType
onCreate: (type: CstType) => void onCreate: (type: CstType) => void
} }
function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) { function CreateCstModal({show, toggle, defaultType, onCreate}: CreateCstModalProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [selectedType, setSelectedType] = useState<CstType|undefined>(undefined); const [selectedType, setSelectedType] = useState<CstType|undefined>(undefined);
@ -18,6 +19,10 @@ function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
if (selectedType) onCreate(selectedType); if (selectedType) onCreate(selectedType);
}; };
useEffect(() => {
setSelectedType(defaultType);
}, [defaultType]);
useEffect(() => { useEffect(() => {
setValidated(selectedType !== undefined); setValidated(selectedType !== undefined);
}, [selectedType] }, [selectedType]
@ -34,6 +39,8 @@ function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
<Select <Select
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
filterOption={null}
value={selectedType && {value: selectedType, label: getCstTypeLabel(selectedType)}}
onChange={(data) => setSelectedType(data?.value)} onChange={(data) => setSelectedType(data?.value)}
/> />
</Modal> </Modal>

View File

@ -12,7 +12,7 @@ import RSFormStats from './RSFormStats';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import TablistTools from './TablistTools'; import TablistTools from './TablistTools';
enum TabsList { export enum RSFormTabsList {
CARD = 0, CARD = 0,
CST_LIST = 1, CST_LIST = 1,
CST_EDIT = 2 CST_EDIT = 2
@ -20,13 +20,13 @@ enum TabsList {
function RSFormTabs() { function RSFormTabs() {
const { setActive, active, error, schema, loading } = useRSForm(); const { setActive, active, error, schema, loading } = useRSForm();
const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', TabsList.CARD); const [tabIndex, setTabIndex] = useLocalStorage('rsform_edit_tab', RSFormTabsList.CARD);
const [init, setInit] = useState(false); const [init, setInit] = useState(false);
const onEditCst = (cst: IConstituenta) => { const onEditCst = (cst: IConstituenta) => {
console.log(`Set active cst: ${cst.alias}`); console.log(`Set active cst: ${cst.alias}`);
setActive(cst); setActive(cst);
setTabIndex(TabsList.CST_EDIT) setTabIndex(RSFormTabsList.CST_EDIT)
}; };
const onSelectTab = (index: number) => { const onSelectTab = (index: number) => {
@ -46,7 +46,7 @@ function RSFormTabs() {
useEffect(() => { useEffect(() => {
const url = new URL(window.location.href); const url = new URL(window.location.href);
const tabQuery = url.searchParams.get('tab'); const tabQuery = url.searchParams.get('tab');
setTabIndex(Number(tabQuery) || TabsList.CARD); setTabIndex(Number(tabQuery) || RSFormTabsList.CARD);
}, [setTabIndex]); }, [setTabIndex]);
useEffect(() => { useEffect(() => {
@ -54,7 +54,7 @@ function RSFormTabs() {
const url = new URL(window.location.href); const url = new URL(window.location.href);
let currentActive = url.searchParams.get('active'); let currentActive = url.searchParams.get('active');
const currentTab = url.searchParams.get('tab'); const currentTab = url.searchParams.get('tab');
const saveHistory = tabIndex === TabsList.CST_EDIT && currentActive !== String(active?.entityUID); const saveHistory = tabIndex === RSFormTabsList.CST_EDIT && currentActive !== String(active?.entityUID);
if (currentTab !== String(tabIndex)) { if (currentTab !== String(tabIndex)) {
url.searchParams.set('tab', String(tabIndex)); url.searchParams.set('tab', String(tabIndex));
} }

View File

@ -15,8 +15,8 @@ function RSFormsTable({schemas}: RSFormsTableProps) {
const intl = useIntl(); const intl = useIntl();
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const openRSForm = (row: IRSForm, event: React.MouseEvent<Element, MouseEvent>) => { const openRSForm = (schema: IRSForm, event: React.MouseEvent<Element, MouseEvent>) => {
navigate(`/rsforms/${row.id}`); navigate(`/rsforms/${schema.id}`);
}; };
const columns = useMemo(() => const columns = useMemo(() =>
@ -68,7 +68,7 @@ function RSFormsTable({schemas}: RSFormsTableProps) {
highlightOnHover highlightOnHover
pointerOnHover pointerOnHover
noDataComponent={<span className='p-2 flex flex-col justify-center text-center'> noDataComponent={<span className='flex flex-col justify-center p-2 text-center'>
<p>Список схем пуст</p> <p>Список схем пуст</p>
<p>Измените фильтр</p> <p>Измените фильтр</p>
</span>} </span>}

View File

@ -35,7 +35,7 @@ function RegisterPage() {
'first_name': firstName, 'first_name': firstName,
'last_name': lastName, 'last_name': lastName,
}; };
signup(data, () => { setSuccess(true); }); signup(data).then(() => setSuccess(true));
} }
}; };

View File

@ -143,7 +143,7 @@ export async function postClaimRSForm(target: string, request?: IFrontRequest) {
} }
export async function postCheckExpression(schema: string, request?: IFrontRequest) { export async function postCheckExpression(schema: string, request?: IFrontRequest) {
AxiosPost({ return AxiosPost({
title: `Check expression for RSForm id=${schema}: ${request?.data['expression']}`, title: `Check expression for RSForm id=${schema}: ${request?.data['expression']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/check/`, endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
request: request request: request
@ -151,84 +151,92 @@ export async function postCheckExpression(schema: string, request?: IFrontReques
} }
export async function postNewConstituenta(schema: string, request?: IFrontRequest) { export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
AxiosPost({ return AxiosPost({
title: `New Constituenta for RSForm id=${schema}: ${request?.data['alias']}`, title: `New Constituenta for RSForm id=${schema}: ${request?.data['alias']}`,
endpoint: `${config.url.BASE}rsforms/${schema}/new-constituenta/`, endpoint: `${config.url.BASE}rsforms/${schema}/cst-create/`,
request: request
});
}
export async function postDeleteConstituenta(schema: string, request?: IFrontRequest) {
return AxiosPost({
title: `Delete Constituents for RSForm id=${schema}: ${request?.data['items'].toString()}`,
endpoint: `${config.url.BASE}rsforms/${schema}/cst-multidelete/`,
request: request request: request
}); });
} }
// ====== Helper functions =========== // ====== Helper functions ===========
function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) { async function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`); if (title) console.log(`[[${title}]] requested`);
if (request?.setLoading) request?.setLoading(true); if (request?.setLoading) request?.setLoading(true);
axios.get<ReturnType>(endpoint) axios.get<ReturnType>(endpoint)
.then(function (response) { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response); if (request?.onSucccess) request.onSucccess(response);
}) })
.catch(function (error) { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message); if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request?.onError) request.onError(error);
}); });
} }
function AxiosGetBlob({endpoint, request, title}: IAxiosRequest) { async function AxiosGetBlob({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] requested`); if (title) console.log(`[[${title}]] requested`);
if (request?.setLoading) request?.setLoading(true); if (request?.setLoading) request?.setLoading(true);
axios.get(endpoint, {responseType: 'blob'}) axios.get(endpoint, {responseType: 'blob'})
.then(function (response) { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response); if (request?.onSucccess) request.onSucccess(response);
}) })
.catch(function (error) { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message); if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request?.onError) request.onError(error);
}); });
} }
function AxiosPost({endpoint, request, title}: IAxiosRequest) { async function AxiosPost({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] posted`); if (title) console.log(`[[${title}]] posted`);
if (request?.setLoading) request?.setLoading(true); if (request?.setLoading) request?.setLoading(true);
axios.post(endpoint, request?.data) axios.post(endpoint, request?.data)
.then(function (response) { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response); if (request?.onSucccess) request.onSucccess(response);
}) })
.catch(function (error) { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message); if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request?.onError) request.onError(error);
}); });
} }
function AxiosDelete({endpoint, request, title}: IAxiosRequest) { async function AxiosDelete({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being deleted`); if (title) console.log(`[[${title}]] is being deleted`);
if (request?.setLoading) request?.setLoading(true); if (request?.setLoading) request?.setLoading(true);
axios.delete(endpoint) axios.delete(endpoint)
.then(function (response) { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response); if (request?.onSucccess) request.onSucccess(response);
}) })
.catch(function (error) { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message); if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request?.onError) request.onError(error);
}); });
} }
function AxiosPatch({endpoint, request, title}: IAxiosRequest) { async function AxiosPatch({endpoint, request, title}: IAxiosRequest) {
if (title) console.log(`[[${title}]] is being patrially updated`); if (title) console.log(`[[${title}]] is being patrially updated`);
if (request?.setLoading) request?.setLoading(true); if (request?.setLoading) request?.setLoading(true);
axios.patch(endpoint, request?.data) axios.patch(endpoint, request?.data)
.then(function (response) { .then((response) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.onSucccess) request.onSucccess(response); if (request?.onSucccess) request.onSucccess(response);
}) })
.catch(function (error) { .catch((error) => {
if (request?.setLoading) request?.setLoading(false); if (request?.setLoading) request?.setLoading(false);
if (request?.showError) toast.error(error.message); if (request?.showError) toast.error(error.message);
if (request?.onError) request.onError(error); if (request?.onError) request.onError(error);

View File

@ -8,8 +8,8 @@ export function shareCurrentURLProc() {
toast.success(`Ссылка скопирована: ${url}`); toast.success(`Ссылка скопирована: ${url}`);
} }
export function claimOwnershipProc( export async function claimOwnershipProc(
claim: (callback: BackendCallback) => void, claim: (callback: BackendCallback) => Promise<void>,
reload: Function reload: Function
) { ) {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) { if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
@ -21,8 +21,8 @@ export function claimOwnershipProc(
}); });
} }
export function deleteRSFormProc( export async function deleteRSFormProc(
destroy: (callback: BackendCallback) => void, destroy: (callback: BackendCallback) => Promise<void>,
navigate: Function navigate: Function
) { ) {
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) { if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
@ -34,8 +34,8 @@ export function deleteRSFormProc(
}); });
} }
export function downloadRSFormProc( export async function downloadRSFormProc(
download: (callback: BackendCallback) => void, download: (callback: BackendCallback) => Promise<void>,
fileName: string fileName: string
) { ) {
download((response) => { download((response) => {