mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Refactor async functions. Implement DeleteCst
This commit is contained in:
parent
e58fd183e9
commit
ffbeafc3f5
|
@ -92,6 +92,7 @@ class RSForm(models.Model):
|
|||
csttype=type
|
||||
)
|
||||
self._recreate_order()
|
||||
self.save()
|
||||
return Constituenta.objects.get(pk=result.pk)
|
||||
|
||||
@transaction.atomic
|
||||
|
@ -107,8 +108,17 @@ class RSForm(models.Model):
|
|||
csttype=type
|
||||
)
|
||||
self._recreate_order()
|
||||
self.save()
|
||||
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
|
||||
@transaction.atomic
|
||||
def import_json(owner: User, data: dict, is_common: bool = True) -> 'RSForm':
|
||||
|
|
|
@ -7,6 +7,12 @@ class FileSerializer(serializers.Serializer):
|
|||
file = serializers.FileField(allow_empty_file=False)
|
||||
|
||||
|
||||
class ItemsListSerlializer(serializers.Serializer):
|
||||
items = serializers.ListField(
|
||||
child=serializers.IntegerField()
|
||||
)
|
||||
|
||||
|
||||
class ExpressionSerializer(serializers.Serializer):
|
||||
expression = serializers.CharField()
|
||||
|
||||
|
|
|
@ -177,6 +177,20 @@ class TestRSForm(TestCase):
|
|||
self.assertEqual(cst2.schema, schema)
|
||||
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):
|
||||
schema = RSForm.objects.create(title='Test', alias='KS1', comment='Test')
|
||||
x1 = schema.insert_at(4, 'X1', CstType.BASE)
|
||||
|
|
|
@ -173,14 +173,14 @@ class TestRSFormViewset(APITestCase):
|
|||
|
||||
def test_create_constituenta(self):
|
||||
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')
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
schema = self.rsform_owned
|
||||
Constituenta.objects.create(schema=schema, alias='X1', csttype='basic', order=1)
|
||||
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')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['alias'], 'X3')
|
||||
|
@ -188,13 +188,38 @@ class TestRSFormViewset(APITestCase):
|
|||
self.assertEqual(x3.order, 3)
|
||||
|
||||
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')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.data['alias'], 'X4')
|
||||
x4 = Constituenta.objects.get(alias=response.data['alias'])
|
||||
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):
|
||||
def setUp(self):
|
||||
|
|
|
@ -42,7 +42,7 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
|
||||
def get_permissions(self):
|
||||
if self.action in ['update', 'destroy', 'partial_update',
|
||||
'new_constituenta']:
|
||||
'cst_create', 'cst_multidelete']:
|
||||
permission_classes = [utils.ObjectOwnerOrAdmin]
|
||||
elif self.action in ['create', 'claim']:
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
@ -50,9 +50,9 @@ class RSFormViewSet(viewsets.ModelViewSet):
|
|||
permission_classes = [permissions.AllowAny]
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='new-constituenta')
|
||||
def new_constituenta(self, request, pk):
|
||||
''' View schema contents (including constituents) '''
|
||||
@action(detail=True, methods=['post'], url_path='cst-create')
|
||||
def cst_create(self, request, pk):
|
||||
''' Create new constituenta '''
|
||||
schema: models.RSForm = self.get_object()
|
||||
serializer = serializers.NewConstituentaSerializer(data=request.data)
|
||||
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'])
|
||||
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'])
|
||||
def claim(self, request, pk=None):
|
||||
schema: models.RSForm = self.get_object()
|
||||
|
|
|
@ -7,9 +7,9 @@ import { getAuth, postLogin, postLogout, postSignup } from '../utils/backendAPI'
|
|||
|
||||
interface IAuthContext {
|
||||
user: ICurrentUser | undefined
|
||||
login: (username: string, password: string, onSuccess?: () => void) => void
|
||||
logout: (onSuccess?: () => void) => void
|
||||
signup: (data: IUserSignupData, onSuccess?: () => void) => void
|
||||
login: (username: string, password: string) => Promise<void>
|
||||
logout: (onSuccess?: () => void) => Promise<void>
|
||||
signup: (data: IUserSignupData) => Promise<void>
|
||||
loading: boolean
|
||||
error: ErrorInfo
|
||||
setError: (error: ErrorInfo) => void
|
||||
|
@ -17,9 +17,9 @@ interface IAuthContext {
|
|||
|
||||
export const AuthContext = createContext<IAuthContext>({
|
||||
user: undefined,
|
||||
login: () => {},
|
||||
logout: () => {},
|
||||
signup: () => {},
|
||||
login: async () => {},
|
||||
logout: async () => {},
|
||||
signup: async () => {},
|
||||
loading: false,
|
||||
error: '',
|
||||
setError: () => {}
|
||||
|
@ -63,18 +63,17 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
|||
});
|
||||
}
|
||||
|
||||
async function logout(onSuccess?: () => void) {
|
||||
async function logout() {
|
||||
setError(undefined);
|
||||
postLogout({
|
||||
showError: true,
|
||||
onSucccess: response => {
|
||||
loadCurrentUser();
|
||||
if(onSuccess) onSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function signup(data: IUserSignupData, onSuccess?: () => void) {
|
||||
async function signup(data: IUserSignupData) {
|
||||
setError(undefined);
|
||||
postSignup({
|
||||
data: data,
|
||||
|
@ -83,7 +82,6 @@ export const AuthState = ({ children }: AuthStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: response => {
|
||||
loadCurrentUser();
|
||||
if(onSuccess) onSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { IConstituenta, IRSForm } from '../utils/models';
|
|||
import { useRSFormDetails } from '../hooks/useRSFormDetails';
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
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';
|
||||
|
||||
interface IRSFormContext {
|
||||
|
@ -23,14 +23,16 @@ interface IRSFormContext {
|
|||
toggleForceAdmin: () => void
|
||||
toggleReadonly: () => 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
|
||||
cstCreate: (data: any, callback: BackendCallback) => void
|
||||
reload: () => Promise<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>({
|
||||
|
@ -50,14 +52,16 @@ export const RSFormContext = createContext<IRSFormContext>({
|
|||
toggleForceAdmin: () => {},
|
||||
toggleReadonly: () => {},
|
||||
toggleTracking: () => {},
|
||||
reload: () => {},
|
||||
update: () => {},
|
||||
destroy: () => {},
|
||||
claim: () => {},
|
||||
download: () => {},
|
||||
|
||||
cstUpdate: () => {},
|
||||
cstCreate: () => {},
|
||||
reload: async () => {},
|
||||
update: async () => {},
|
||||
destroy: async () => {},
|
||||
claim: async () => {},
|
||||
download: async () => {},
|
||||
|
||||
cstUpdate: async () => {},
|
||||
cstCreate: async () => {},
|
||||
cstDelete: async () => {},
|
||||
})
|
||||
|
||||
interface RSFormStateProps {
|
||||
|
@ -76,22 +80,26 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
|
||||
const isOwned = 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 (
|
||||
!readonly &&
|
||||
!loading && !readonly &&
|
||||
(isOwned || (forceAdmin && user?.is_staff) || false)
|
||||
)
|
||||
}, [user, readonly, forceAdmin, isOwned]);
|
||||
}, [user, readonly, forceAdmin, isOwned, loading]);
|
||||
|
||||
const isTracking = useMemo(() => {
|
||||
const isTracking = useMemo(
|
||||
() => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const toggleTracking = useCallback(() => {
|
||||
const toggleTracking = useCallback(
|
||||
() => {
|
||||
toast('not implemented yet');
|
||||
}, []);
|
||||
|
||||
async function update(data: any, callback?: BackendCallback) {
|
||||
const update = useCallback(
|
||||
async (data: any, callback?: BackendCallback) => {
|
||||
setError(undefined);
|
||||
patchRSForm(schemaID, {
|
||||
data: data,
|
||||
|
@ -100,9 +108,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: callback
|
||||
});
|
||||
}
|
||||
}, [schemaID, setError]);
|
||||
|
||||
async function destroy(callback: BackendCallback) {
|
||||
const destroy = useCallback(
|
||||
async (callback?: BackendCallback) => {
|
||||
setError(undefined);
|
||||
deleteRSForm(schemaID, {
|
||||
showError: true,
|
||||
|
@ -110,9 +119,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: callback
|
||||
});
|
||||
}
|
||||
}, [schemaID, setError]);
|
||||
|
||||
async function claim(callback: BackendCallback) {
|
||||
const claim = useCallback(
|
||||
async (callback?: BackendCallback) => {
|
||||
setError(undefined);
|
||||
postClaimRSForm(schemaID, {
|
||||
showError: true,
|
||||
|
@ -120,9 +130,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: callback
|
||||
});
|
||||
}
|
||||
}, [schemaID, setError]);
|
||||
|
||||
async function download(callback: BackendCallback) {
|
||||
const download = useCallback(
|
||||
async (callback: BackendCallback) => {
|
||||
setError(undefined);
|
||||
getTRSFile(schemaID, {
|
||||
showError: true,
|
||||
|
@ -130,9 +141,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: callback
|
||||
});
|
||||
}
|
||||
}, [schemaID, setError]);
|
||||
|
||||
async function cstUpdate(data: any, callback?: BackendCallback) {
|
||||
const cstUpdate = useCallback(
|
||||
async (data: any, callback?: BackendCallback) => {
|
||||
setError(undefined);
|
||||
patchConstituenta(String(active!.entityUID), {
|
||||
data: data,
|
||||
|
@ -141,9 +153,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
onSucccess: callback
|
||||
});
|
||||
}
|
||||
}, [active, setError]);
|
||||
|
||||
async function cstCreate(data: any, callback?: BackendCallback) {
|
||||
const cstCreate = useCallback(
|
||||
async (data: any, callback?: BackendCallback) => {
|
||||
setError(undefined);
|
||||
postNewConstituenta(schemaID, {
|
||||
data: data,
|
||||
|
@ -152,7 +165,19 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
onError: error => setError(error),
|
||||
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 (
|
||||
<RSFormContext.Provider value={{
|
||||
|
@ -164,7 +189,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
|
|||
isOwned, isEditable, isClaimable,
|
||||
isTracking, toggleTracking,
|
||||
reload, update, download, destroy, claim,
|
||||
cstUpdate, cstCreate
|
||||
cstUpdate, cstCreate, cstDelete,
|
||||
}}>
|
||||
{ children }
|
||||
</RSFormContext.Provider>
|
||||
|
|
|
@ -5,13 +5,13 @@ import { getActiveUsers } from '../utils/backendAPI'
|
|||
|
||||
interface IUsersContext {
|
||||
users: IUserInfo[]
|
||||
reload: () => void
|
||||
reload: () => Promise<void>
|
||||
getUserLabel: (userID?: number) => string
|
||||
}
|
||||
|
||||
export const UsersContext = createContext<IUsersContext>({
|
||||
users: [],
|
||||
reload: () => {},
|
||||
reload: async () => {},
|
||||
getUserLabel: () => ''
|
||||
})
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ export function useRSFormDetails({target}: {target?: string}) {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
const fetchData = useCallback(
|
||||
async () => {
|
||||
setError(undefined);
|
||||
setSchema(undefined);
|
||||
if (!target) {
|
||||
|
@ -20,17 +21,18 @@ export function useRSFormDetails({target}: {target?: string}) {
|
|||
onError: error => setError(error),
|
||||
onSucccess: (response) => {
|
||||
CalculateStats(response.data)
|
||||
console.log(response.data);
|
||||
setSchema(response.data);
|
||||
}
|
||||
});
|
||||
}, [target]);
|
||||
|
||||
async function reload() {
|
||||
fetchData()
|
||||
fetchData();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
fetchData();
|
||||
}, [fetchData])
|
||||
|
||||
return { schema, reload, error, setError, loading };
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const {user} = useAuth();
|
||||
if (user) {
|
||||
navigate('/rsforms?filter=personal');
|
||||
} else {
|
||||
navigate('/rsforms?filter=common');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center w-full py-2'>
|
||||
<p>Home page</p>
|
||||
|
|
|
@ -30,7 +30,8 @@ function LoginPage() {
|
|||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!loading) {
|
||||
login(username, password, () => { navigate('/rsforms?filter=personal'); });
|
||||
login(username, password)
|
||||
.then(() => navigate('/rsforms?filter=personal'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ export function NotFoundPage() {
|
|||
return (
|
||||
<div>
|
||||
<h1 className='text-xl font-semibold'>Error 404 - Not Found</h1>
|
||||
<p className='mt-2'>Данная страница не существует</p>
|
||||
<p className='mt-2'>Данная страница не существует или запрашиваемый объект отсутствует в базы данных</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import { EditMode } from '../../utils/models';
|
||||
import { CstType, EditMode, INewCstData } from '../../utils/models';
|
||||
import { toast } from 'react-toastify';
|
||||
import TextArea from '../../components/Common/TextArea';
|
||||
import ExpressionEditor from './ExpressionEditor';
|
||||
import SubmitButton from '../../components/Common/SubmitButton';
|
||||
import { getCstTypeLabel } from '../../utils/staticUI';
|
||||
import { createAliasFor, getCstTypeLabel } from '../../utils/staticUI';
|
||||
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() {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
active, schema, setActive, processing, cstUpdate, isEditable, reload
|
||||
active, schema, setActive, processing, isEditable, reload,
|
||||
cstDelete, cstUpdate, cstCreate
|
||||
} = useRSForm();
|
||||
|
||||
const [showCstModal, setShowCstModal] = useState(false);
|
||||
const [editMode, setEditMode] = useState(EditMode.TEXT);
|
||||
|
||||
const [alias, setAlias] = useState('');
|
||||
|
@ -42,7 +49,8 @@ function ConstituentEditor() {
|
|||
}
|
||||
}, [active]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
const handleSubmit =
|
||||
async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (!processing) {
|
||||
const data = {
|
||||
|
@ -59,14 +67,51 @@ function ConstituentEditor() {
|
|||
'forms': active?.term?.forms || [],
|
||||
}
|
||||
};
|
||||
cstUpdate(data, (response) => {
|
||||
console.log(response);
|
||||
cstUpdate(data)
|
||||
.then(() => {
|
||||
toast.success('Изменения сохранены');
|
||||
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(() => {
|
||||
toast.info('Переименование в разработке');
|
||||
}, []);
|
||||
|
@ -78,8 +123,21 @@ function ConstituentEditor() {
|
|||
|
||||
return (
|
||||
<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'>
|
||||
<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'>
|
||||
<span className='mr-12'>
|
||||
<label
|
||||
|
@ -103,13 +161,22 @@ function ConstituentEditor() {
|
|||
</span>
|
||||
</div>
|
||||
<div className='flex justify-end'>
|
||||
<button type='submit'
|
||||
title='Сохранить изменения'
|
||||
className={'px-1 py-1 whitespace-nowrap font-bold disabled:cursor-not-allowed rounded clr-btn-primary'}
|
||||
disabled={!isEditable}
|
||||
>
|
||||
<SaveIcon size={5} />
|
||||
</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={() => handleAddNew()}
|
||||
>
|
||||
<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>
|
||||
<TextArea id='term' label='Термин'
|
||||
|
|
|
@ -129,7 +129,7 @@ function ConstituentsSideList({expression}: ConstituentsSideListProps) {
|
|||
columns={columns}
|
||||
keyField='id'
|
||||
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>
|
||||
</span>}
|
||||
|
|
|
@ -15,9 +15,9 @@ interface ConstituentsTableProps {
|
|||
}
|
||||
|
||||
function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
||||
const { schema, isEditable, cstCreate, reload } = useRSForm();
|
||||
const [selectedRows, setSelectedRows] = useState<IConstituenta[]>([]);
|
||||
const nothingSelected = useMemo(() => selectedRows.length === 0, [selectedRows]);
|
||||
const { schema, isEditable, cstCreate, cstDelete, reload } = useRSForm();
|
||||
const [selected, setSelected] = useState<IConstituenta[]>([]);
|
||||
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
|
||||
|
||||
const [showCstModal, setShowCstModal] = useState(false);
|
||||
|
||||
|
@ -29,11 +29,21 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
|||
}, [onOpenEdit]);
|
||||
|
||||
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(() => {
|
||||
toast.info('Перемещение вверх');
|
||||
|
||||
}, []);
|
||||
|
||||
const handleMoveDown = useCallback(() => {
|
||||
|
@ -49,18 +59,17 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
|||
setShowCstModal(true);
|
||||
} else {
|
||||
let data: INewCstData = {
|
||||
csttype: csttype,
|
||||
alias: createAliasFor(csttype, schema!)
|
||||
'csttype': csttype,
|
||||
'alias': createAliasFor(csttype, schema!)
|
||||
}
|
||||
if (selectedRows.length > 0) {
|
||||
data['insert_after'] = selectedRows[selectedRows.length - 1].entityUID
|
||||
if (selected.length > 0) {
|
||||
data['insert_after'] = selected[selected.length - 1].entityUID
|
||||
}
|
||||
cstCreate(data, (response: AxiosResponse) => {
|
||||
reload();
|
||||
toast.info(`Добавлена конституента ${response.data['alias']}`);
|
||||
reload().then(() => toast.success(`Добавлена конституента ${response.data['alias']}`));
|
||||
});
|
||||
}
|
||||
}, [schema, selectedRows, reload, cstCreate]);
|
||||
}, [schema, selected, reload, cstCreate]);
|
||||
|
||||
const columns = useMemo(() =>
|
||||
[
|
||||
|
@ -182,7 +191,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
|||
/>
|
||||
<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='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'>
|
||||
<Button
|
||||
tooltip='Переместить вверх'
|
||||
|
@ -247,7 +256,7 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
|
|||
|
||||
selectableRows
|
||||
selectableRowsHighlight
|
||||
onSelectedRowsChange={({selectedRows}) => setSelectedRows(selectedRows)}
|
||||
onSelectedRowsChange={({selectedRows}) => setSelected(selectedRows)}
|
||||
onRowDoubleClicked={onOpenEdit}
|
||||
onRowClicked={handleRowClicked}
|
||||
dense
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import Modal from '../../components/Common/Modal';
|
||||
import { CstType } from '../../utils/models';
|
||||
import Select from 'react-select';
|
||||
import { CstTypeSelector } from '../../utils/staticUI';
|
||||
import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface CreateCstModalProps {
|
||||
show: boolean
|
||||
toggle: () => void
|
||||
defaultType?: CstType
|
||||
onCreate: (type: CstType) => void
|
||||
}
|
||||
|
||||
function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
|
||||
function CreateCstModal({show, toggle, defaultType, onCreate}: CreateCstModalProps) {
|
||||
const [validated, setValidated] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<CstType|undefined>(undefined);
|
||||
|
||||
|
@ -18,6 +19,10 @@ function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
|
|||
if (selectedType) onCreate(selectedType);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedType(defaultType);
|
||||
}, [defaultType]);
|
||||
|
||||
useEffect(() => {
|
||||
setValidated(selectedType !== undefined);
|
||||
}, [selectedType]
|
||||
|
@ -34,6 +39,8 @@ function CreateCstModal({show, toggle, onCreate}: CreateCstModalProps) {
|
|||
<Select
|
||||
options={CstTypeSelector}
|
||||
placeholder='Выберите тип'
|
||||
filterOption={null}
|
||||
value={selectedType && {value: selectedType, label: getCstTypeLabel(selectedType)}}
|
||||
onChange={(data) => setSelectedType(data?.value)}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -12,7 +12,7 @@ import RSFormStats from './RSFormStats';
|
|||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import TablistTools from './TablistTools';
|
||||
|
||||
enum TabsList {
|
||||
export enum RSFormTabsList {
|
||||
CARD = 0,
|
||||
CST_LIST = 1,
|
||||
CST_EDIT = 2
|
||||
|
@ -20,13 +20,13 @@ enum TabsList {
|
|||
|
||||
function RSFormTabs() {
|
||||
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 onEditCst = (cst: IConstituenta) => {
|
||||
console.log(`Set active cst: ${cst.alias}`);
|
||||
setActive(cst);
|
||||
setTabIndex(TabsList.CST_EDIT)
|
||||
setTabIndex(RSFormTabsList.CST_EDIT)
|
||||
};
|
||||
|
||||
const onSelectTab = (index: number) => {
|
||||
|
@ -46,7 +46,7 @@ function RSFormTabs() {
|
|||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
const tabQuery = url.searchParams.get('tab');
|
||||
setTabIndex(Number(tabQuery) || TabsList.CARD);
|
||||
setTabIndex(Number(tabQuery) || RSFormTabsList.CARD);
|
||||
}, [setTabIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -54,7 +54,7 @@ function RSFormTabs() {
|
|||
const url = new URL(window.location.href);
|
||||
let currentActive = url.searchParams.get('active');
|
||||
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)) {
|
||||
url.searchParams.set('tab', String(tabIndex));
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ function RSFormsTable({schemas}: RSFormsTableProps) {
|
|||
const intl = useIntl();
|
||||
const { getUserLabel } = useUsers();
|
||||
|
||||
const openRSForm = (row: IRSForm, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
navigate(`/rsforms/${row.id}`);
|
||||
const openRSForm = (schema: IRSForm, event: React.MouseEvent<Element, MouseEvent>) => {
|
||||
navigate(`/rsforms/${schema.id}`);
|
||||
};
|
||||
|
||||
const columns = useMemo(() =>
|
||||
|
@ -68,7 +68,7 @@ function RSFormsTable({schemas}: RSFormsTableProps) {
|
|||
highlightOnHover
|
||||
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>
|
||||
</span>}
|
||||
|
|
|
@ -35,7 +35,7 @@ function RegisterPage() {
|
|||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
};
|
||||
signup(data, () => { setSuccess(true); });
|
||||
signup(data).then(() => setSuccess(true));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -143,7 +143,7 @@ export async function postClaimRSForm(target: string, request?: IFrontRequest) {
|
|||
}
|
||||
|
||||
export async function postCheckExpression(schema: string, request?: IFrontRequest) {
|
||||
AxiosPost({
|
||||
return AxiosPost({
|
||||
title: `Check expression for RSForm id=${schema}: ${request?.data['expression']}`,
|
||||
endpoint: `${config.url.BASE}rsforms/${schema}/check/`,
|
||||
request: request
|
||||
|
@ -151,84 +151,92 @@ export async function postCheckExpression(schema: string, request?: IFrontReques
|
|||
}
|
||||
|
||||
export async function postNewConstituenta(schema: string, request?: IFrontRequest) {
|
||||
AxiosPost({
|
||||
return AxiosPost({
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ====== Helper functions ===========
|
||||
function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {
|
||||
async function AxiosGet<ReturnType>({endpoint, request, title}: IAxiosRequest) {
|
||||
if (title) console.log(`[[${title}]] requested`);
|
||||
if (request?.setLoading) request?.setLoading(true);
|
||||
axios.get<ReturnType>(endpoint)
|
||||
.then(function (response) {
|
||||
.then((response) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.onSucccess) request.onSucccess(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch((error) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.showError) toast.error(error.message);
|
||||
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 (request?.setLoading) request?.setLoading(true);
|
||||
axios.get(endpoint, {responseType: 'blob'})
|
||||
.then(function (response) {
|
||||
.then((response) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.onSucccess) request.onSucccess(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch((error) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.showError) toast.error(error.message);
|
||||
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 (request?.setLoading) request?.setLoading(true);
|
||||
axios.post(endpoint, request?.data)
|
||||
.then(function (response) {
|
||||
.then((response) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.onSucccess) request.onSucccess(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch((error) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.showError) toast.error(error.message);
|
||||
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 (request?.setLoading) request?.setLoading(true);
|
||||
axios.delete(endpoint)
|
||||
.then(function (response) {
|
||||
.then((response) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.onSucccess) request.onSucccess(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch((error) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.showError) toast.error(error.message);
|
||||
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 (request?.setLoading) request?.setLoading(true);
|
||||
axios.patch(endpoint, request?.data)
|
||||
.then(function (response) {
|
||||
.then((response) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.onSucccess) request.onSucccess(response);
|
||||
})
|
||||
.catch(function (error) {
|
||||
.catch((error) => {
|
||||
if (request?.setLoading) request?.setLoading(false);
|
||||
if (request?.showError) toast.error(error.message);
|
||||
if (request?.onError) request.onError(error);
|
||||
|
|
|
@ -8,8 +8,8 @@ export function shareCurrentURLProc() {
|
|||
toast.success(`Ссылка скопирована: ${url}`);
|
||||
}
|
||||
|
||||
export function claimOwnershipProc(
|
||||
claim: (callback: BackendCallback) => void,
|
||||
export async function claimOwnershipProc(
|
||||
claim: (callback: BackendCallback) => Promise<void>,
|
||||
reload: Function
|
||||
) {
|
||||
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
|
||||
|
@ -21,8 +21,8 @@ export function claimOwnershipProc(
|
|||
});
|
||||
}
|
||||
|
||||
export function deleteRSFormProc(
|
||||
destroy: (callback: BackendCallback) => void,
|
||||
export async function deleteRSFormProc(
|
||||
destroy: (callback: BackendCallback) => Promise<void>,
|
||||
navigate: Function
|
||||
) {
|
||||
if (!window.confirm('Вы уверены, что хотите удалить данную схему?')) {
|
||||
|
@ -34,8 +34,8 @@ export function deleteRSFormProc(
|
|||
});
|
||||
}
|
||||
|
||||
export function downloadRSFormProc(
|
||||
download: (callback: BackendCallback) => void,
|
||||
export async function downloadRSFormProc(
|
||||
download: (callback: BackendCallback) => Promise<void>,
|
||||
fileName: string
|
||||
) {
|
||||
download((response) => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user